Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions .github/workflows/prerelease.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
name: Prerelease candidate

on:
schedule:
- cron: "23 6 * * *"
workflow_dispatch:
inputs:
channel:
description: "Candidate channel"
type: choice
options: [beta, nightly]
default: beta
required: true
beta_number:
description: "Positive beta sequence, required for beta"
type: string
required: false
dry_run:
description: "Keep artifacts in Actions without creating a release draft"
type: boolean
default: true
required: true

permissions:
contents: read

concurrency:
group: prerelease-candidate
cancel-in-progress: false

jobs:
select:
runs-on: ubuntu-latest
outputs:
changed: ${{ steps.candidate.outputs.changed }}
version: ${{ steps.candidate.outputs.version }}
sha: ${{ steps.candidate.outputs.sha }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10
with:
ref: dev
fetch-depth: 0
persist-credentials: false
- name: Select immutable candidate
id: candidate
shell: bash
env:
CHANNEL: ${{ inputs.channel || 'nightly' }}
BETA_NUMBER: ${{ inputs.beta_number }}
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: |
set -euo pipefail
sha="$(git rev-parse HEAD)"
base="$(node -p 'JSON.parse(require("fs").readFileSync("src/mendcode/packages/opencode/package.json", "utf8")).version')"
[[ "$base" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "Package version must be stable semver" >&2; exit 1; }
if [ "$CHANNEL" = nightly ]; then
# Tags are never moved. Any successful draft on this source already
# represents the daily candidate, including reruns and manual runs.
previous="$(gh api "repos/${GH_REPO}/releases?per_page=100" --jq '.[] | select(.tag_name | test("^v[0-9]+\\.[0-9]+\\.[0-9]+-nightly\\.")) | .target_commitish')"
if git tag --points-at "$sha" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+-nightly\.' || printf '%s\n' "$previous" | grep -Fxq "$sha"; then
echo "changed=false" >> "$GITHUB_OUTPUT"
exit 0
fi
version="${base}-nightly.$(date -u +%Y%m%d).${GITHUB_RUN_ID}"
elif [ "$CHANNEL" = beta ]; then
[[ "$BETA_NUMBER" =~ ^[1-9][0-9]*$ ]] || { echo "beta_number must be positive" >&2; exit 1; }
version="${base}-beta.${BETA_NUMBER}"
else
echo "Unsupported prerelease channel" >&2
exit 1
fi
echo "changed=true" >> "$GITHUB_OUTPUT"
echo "sha=$sha" >> "$GITHUB_OUTPUT"
echo "version=$version" >> "$GITHUB_OUTPUT"

build:
needs: select
if: needs.select.outputs.changed == 'true'
permissions:
contents: write
attestations: write
id-token: write
uses: ./.github/workflows/release.yml
with:
version: ${{ needs.select.outputs.version }}
source_sha: ${{ needs.select.outputs.sha }}
prerelease: true
# Scheduled runs create reviewable drafts. Publication still requires the
# platform acceptance gates and an explicit promotion; never age-based.
dry_run: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run }}
104 changes: 97 additions & 7 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,24 @@ on:
required: true
default: false
type: boolean
source_sha:
description: "Exact candidate commit already tested; required when publishing"
required: true
type: string
workflow_call:
inputs:
version:
required: true
type: string
dry_run:
required: true
type: boolean
prerelease:
required: true
type: boolean
source_sha:
required: true
type: string

permissions:
contents: read
Expand Down Expand Up @@ -50,23 +68,49 @@ jobs:
echo "version must be semver without leading v: ${VERSION}" >&2
exit 1
fi
if [[ ! "${SOURCE_SHA}" =~ ^[a-f0-9]{40}$ ]]; then
echo "source_sha must be the exact candidate commit" >&2
exit 1
fi
env:
SOURCE_SHA: ${{ inputs.source_sha }}

- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10
with:
fetch-depth: 0
persist-credentials: false
ref: ${{ inputs.source_sha }}

- name: Ensure release runs from main
if: ${{ inputs.dry_run == false }}
shell: bash
run: |
set -euo pipefail
branch="${GITHUB_REF_NAME:-}"
if [ "$branch" != "main" ]; then
if [ "$PRERELEASE" = "false" ] && [ "$branch" != "main" ]; then
echo "publishing releases is only allowed from main; current ref is ${branch}" >&2
exit 1
fi
if [ "$PRERELEASE" = "false" ]; then
git merge-base --is-ancestor HEAD origin/main
else
git merge-base --is-ancestor HEAD origin/dev
fi
env:
PRERELEASE: ${{ inputs.prerelease }}

- name: Validate channel and immutable version
shell: bash
env:
PRERELEASE: ${{ inputs.prerelease }}
run: |
set -euo pipefail
node --input-type=module -e 'import {releaseChannel} from "./src/mendcode/script/release-index.mjs"; if ((releaseChannel(process.env.VERSION) !== "stable") !== (process.env.PRERELEASE === "true")) throw Error("Version and prerelease flag disagree")'
if git rev-parse --verify "refs/tags/v${VERSION}" >/dev/null 2>&1; then
echo "Release tag already exists; releases are immutable" >&2
exit 1
fi

- name: Supply-chain preflight
shell: bash
Expand Down Expand Up @@ -106,6 +150,18 @@ jobs:
set -euo pipefail
bun run --cwd packages/opencode script/build.ts --release-assets --skip-embed-web-ui --skip-install --exclude-os=darwin

- name: Verify release contracts and types
working-directory: src/mendcode
shell: bash
run: |
set -euo pipefail
node --test script/release-index.test.mjs
bun run --cwd packages/opencode typecheck
cd packages/opencode
for test_file in test/installation/*.test.ts test/cli/upgrade-channel.test.ts test/cli/tui/shared-server.test.ts test/cli/tui/thread.test.ts; do
bash -c 'export MENDCODE_DB="${TMPDIR:-/tmp}/opencode-test-data-$$/share/release.db"; exec bun test --timeout 30000 "$@"' bash "$test_file"
done

- name: Upload release artifacts to workflow
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
Expand Down Expand Up @@ -146,6 +202,7 @@ jobs:
with:
fetch-depth: 0
persist-credentials: false
ref: ${{ inputs.source_sha }}

- name: Setup pnpm
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1
Expand Down Expand Up @@ -194,6 +251,17 @@ jobs:
Compress-Archive -Path "$($_.FullName)/bin/*" -DestinationPath "packages/opencode/dist/$($_.Name).zip" -Force
}

- name: Verify native Windows installer recovery and failure preservation
working-directory: src/mendcode
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$candidate = Get-ChildItem packages/opencode/dist/mendcode-windows-${{ matrix.arch }}*/bin/mendcode.exe | Select-Object -First 1
if (-not $candidate) { throw "Native installer test executable is missing" }
$env:MENDCODE_INSTALLER_TEST_BINARY = $candidate.FullName
bun run script/windows-installer-smoke.ts
if ($LASTEXITCODE -ne 0) { throw "Native installer acceptance failed" }

- name: Upload Windows release artifacts to workflow
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
Expand Down Expand Up @@ -228,6 +296,7 @@ jobs:
with:
fetch-depth: 0
persist-credentials: false
ref: ${{ inputs.source_sha }}

- name: Setup pnpm
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1
Expand Down Expand Up @@ -295,6 +364,7 @@ jobs:
with:
fetch-depth: 0
persist-credentials: false
ref: ${{ inputs.source_sha }}

- name: Download platform release assets
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
Expand Down Expand Up @@ -322,12 +392,22 @@ jobs:
{
echo "version=${VERSION}"
echo "repo=${GITHUB_REPOSITORY}"
echo "sha=${GITHUB_SHA}"
echo "sha=${RELEASE_COMMIT}"
echo "run=${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
echo "created=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo
cat SHA256SUMS
} > RELEASE-MANIFEST.txt
env:
RELEASE_COMMIT: ${{ inputs.source_sha }}

- name: Write versioned release index
env:
RELEASE_COMMIT: ${{ inputs.source_sha }}
run: |
set -euo pipefail
test "$(git rev-parse HEAD)" = "$RELEASE_COMMIT"
node src/mendcode/script/release-index.mjs dist

- name: Generate SBOM
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610
Expand All @@ -345,6 +425,7 @@ jobs:
dist/*.tar.gz
dist/SHA256SUMS
dist/RELEASE-MANIFEST.txt
dist/release-index.json
dist/mendcode.spdx.json

- name: Upload assembled release artifacts to workflow
Expand Down Expand Up @@ -372,6 +453,7 @@ jobs:
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10
with:
persist-credentials: false
ref: ${{ inputs.source_sha }}

- name: Download release artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
Expand Down Expand Up @@ -402,21 +484,24 @@ jobs:
fi
done < "${GITHUB_WORKSPACE}/CHANGELOG.md"
} > "$notes"
if [[ "$section_found" != true ]]; then
if [[ "$section_found" != true && "$PRERELEASE" != "true" ]]; then
echo "Missing CHANGELOG.md section for ${VERSION}" >&2
exit 1
fi
if [[ "$PRERELEASE" == "true" ]]; then
printf '\nExperimental prerelease from %s. Concurrency remains opt-in.\n' "$RELEASE_COMMIT" >> "$notes"
fi
cat >> "$notes" <<EOF

## Verify

Built from \`${GITHUB_SHA}\`.
Built from \`${RELEASE_COMMIT}\`.

\`\`\`bash
gh release download ${tag} --repo ${GITHUB_REPOSITORY}
shasum -a 256 -c SHA256SUMS
for file in mendcode-*.zip mendcode-*.tar.gz SHA256SUMS RELEASE-MANIFEST.txt mendcode.spdx.json; do
gh attestation verify --repo ${GITHUB_REPOSITORY} "$file"
for file in mendcode-*.zip mendcode-*.tar.gz SHA256SUMS RELEASE-MANIFEST.txt release-index.json mendcode.spdx.json; do
gh attestation verify --repo ${GITHUB_REPOSITORY} "\$file"
done
\`\`\`
EOF
Expand All @@ -425,10 +510,15 @@ jobs:
*.tar.gz \
SHA256SUMS \
RELEASE-MANIFEST.txt \
release-index.json \
mendcode.spdx.json \
--repo "${GITHUB_REPOSITORY}" \
--target "${GITHUB_SHA}" \
--target "${RELEASE_COMMIT}" \
--title "$tag" \
--notes-file "$notes" \
--draft \
--latest=false \
${{ inputs.prerelease && '--prerelease' || '' }}
env:
RELEASE_COMMIT: ${{ inputs.source_sha }}
PRERELEASE: ${{ inputs.prerelease }}
71 changes: 71 additions & 0 deletions .github/workflows/runtime-recovery.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
name: Runtime and recovery tests

on:
pull_request:
paths:
- "src/mendcode/**"
- ".github/workflows/runtime-recovery.yml"
workflow_dispatch:

permissions:
contents: read

concurrency:
group: runtime-recovery-${{ github.ref }}
cancel-in-progress: true

jobs:
contracts:
name: Linux runtime and recovery contracts
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1
with:
version: 11.0.9
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6
with:
bun-version-file: src/mendcode/package.json
- name: Install frozen dependencies
working-directory: src/mendcode
run: pnpm --pm-on-fail=ignore install --frozen-lockfile --ignore-scripts
- name: Prepare deterministic test runtime
working-directory: src/mendcode/packages/opencode
run: |
bun run script/fix-node-pty.ts
MODELS_DEV_API_JSON="$PWD/test/tool/fixtures/models-api.json" bun run script/generate.ts
- name: Typecheck
working-directory: src/mendcode
run: bun run --cwd packages/opencode typecheck
- name: Release index contracts
working-directory: src/mendcode
run: node --test script/release-index.test.mjs
- name: Runtime and updater regressions
working-directory: src/mendcode/packages/opencode
shell: bash
run: |
set -euo pipefail
# Some suites replace Bun modules globally. Separate processes keep
# those mocks and the test/preload.ts database roots isolated.
run_test() {
bash -c 'export MENDCODE_DB="${TMPDIR:-/tmp}/opencode-test-data-$$/share/contracts.db"; exec bun test --timeout 30000 "$@"' bash "$@"
}
for test_file in \
test/installation/*.test.ts \
test/storage/compatibility.test.ts test/storage/writer-lease.test.ts \
test/cli/run-attach.test.ts test/cli/upgrade-channel.test.ts test/cli/upgrade-readonly.test.ts \
test/cli/tui/shared-server.test.ts test/cli/tui/thread.test.ts \
test/cli/tui/widgets-runtime.test.tsx test/cli/tui/widgets-tray.test.tsx \
test/cli/tui/agent-command-panel.test.tsx \
test/mend/concurrent-model-policy.test.ts \
test/plugin/auth-override.test.ts \
test/session/continuity.test.ts test/session/session-notes.test.ts test/session/llm.test.ts \
test/server/usage.test.ts test/server/release-channel.test.ts test/server/upgrade-progress.test.ts; do
run_test "$test_file"
done
run_test test/session/prompt.test.ts --test-name-pattern "continuity wakes the existing executor"
Loading
Loading