Skip to content

Commit 204a5da

Browse files
committed
Merge branch 'main' into fix_bedrock_proxy
2 parents cfd7c96 + c39535e commit 204a5da

221 files changed

Lines changed: 9466 additions & 2970 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"zoo-code": patch
3+
---
4+
5+
Fix Anthropic provider silently replacing a custom/unrecognized `apiModelId` with the hardcoded default model.
6+
7+
`AnthropicHandler.getModel()` coerced any `apiModelId` not present in the static `anthropicModels` table down to `anthropicDefaultModelId` ("claude-sonnet-4-5"), and that coerced id was what actually got sent as `model` in the API request -- silently ignoring a user-configured custom model name (e.g. a custom Anthropic-compatible deployment or proxy). This produced confusing "model does not exist" errors for the default model instead of the model the user actually selected (#418).
8+
9+
The same fallback also affected capability lookups used to build the `thinking` request parameter: an unrecognized id fell back to the default model's info, which can be from an older model generation with a different API contract, causing the request to use the legacy `thinking: {type: "enabled", budget_tokens}` shape and get rejected with a 400 by models that require `{type: "adaptive"}`.
10+
11+
The model id sent to the API now always honors a user-configured `apiModelId`. For unrecognized values, capabilities are best-effort guessed by matching known model-family substrings (mirroring the existing `BedrockHandler.guessModelInfoFromId` heuristic) instead of defaulting to `anthropicDefaultModelId`'s info.

.github/workflows/cli-release.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ on:
1313
type: boolean
1414
default: false
1515

16+
# Least privilege: the release job escalates to contents: write via its own
17+
# job-level permissions block.
18+
permissions:
19+
contents: read
20+
1621
jobs:
1722
# Build CLI for each platform.
1823
build:

.github/workflows/code-qa.yml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,56 @@ on:
99
merge_group:
1010
types: [checks_requested]
1111

12+
# Least privilege: every job below escalates only where it needs to.
13+
permissions:
14+
contents: read
15+
1216
jobs:
17+
dependency-review:
18+
runs-on: ubuntu-latest
19+
# Only meaningful for PRs — validates the dependency diff of the pull
20+
# request against GitHub's advisory database before merge.
21+
if: github.event_name == 'pull_request'
22+
steps:
23+
- name: Checkout code
24+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
25+
with:
26+
# This job never pushes — don't persist the GITHUB_TOKEN.
27+
persist-credentials: false
28+
- name: Dependency review
29+
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
30+
31+
invisible-chars:
32+
runs-on: ubuntu-latest
33+
# Reject invisible / homoglyph Unicode that GitHub's diff UI renders
34+
# invisibly and most editors hide. These compile fine, which is the
35+
# risk: identifier-splitting, string-literal injection, and the
36+
# "Trojan Source" bidi-override attack (U+202A-U+202E). Scanning raw
37+
# bytes catches them in strings, identifiers, and comments alike.
38+
steps:
39+
- name: Checkout code
40+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
41+
with:
42+
# This job never pushes — don't persist the GITHUB_TOKEN.
43+
persist-credentials: false
44+
- name: Reject invisible / homoglyph Unicode
45+
run: |
46+
# zero-width (U+200B-200F), word joiner (U+2060), BOM (U+FEFF),
47+
# bidi overrides (U+202A-202E), soft hyphen (U+00AD).
48+
# Covers source, release-adjacent executable scripts
49+
# (*.sh / *.cjs / *.cts / *.mts), and the executable shell
50+
# blocks inside GitHub workflow/action YAML.
51+
if grep -rnP '[\x{200B}-\x{200F}\x{202A}-\x{202E}\x{2060}\x{FEFF}\x{00AD}]' \
52+
--include='*.ts' --include='*.tsx' --include='*.js' --include='*.mjs' \
53+
--include='*.cjs' --include='*.cts' --include='*.mts' --include='*.sh' \
54+
--include='*.yml' --include='*.yaml' \
55+
--exclude-dir=node_modules --exclude-dir=dist --exclude-dir=out \
56+
--exclude-dir=coverage --exclude-dir=.turbo --exclude-dir=.vinxi \
57+
src webview-ui packages apps .github; then
58+
echo "::error::Found invisible or homoglyph Unicode characters (zero-width / bidi-override / BOM / soft hyphen)"
59+
exit 1
60+
fi
61+
1362
check-translations:
1463
runs-on: ubuntu-latest
1564
steps:

.github/workflows/codeql.yml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ on:
77
schedule:
88
- cron: '24 19 * * 3'
99

10+
# Least privilege: the analyze job escalates to security-events: write via its
11+
# own job-level permissions block.
12+
permissions:
13+
contents: read
14+
1015
jobs:
1116
analyze:
1217
name: Analyze (${{ matrix.language }})
@@ -47,7 +52,7 @@ jobs:
4752

4853
# Initializes the CodeQL tools for scanning.
4954
- name: Initialize CodeQL
50-
uses: github/codeql-action/init@dd903d2e4f5405488e5ef1422510ee31c8b32357 # v3
55+
uses: github/codeql-action/init@411c4c9a36b3fca4d674f06b6396b2c6d23522c6 # v3
5156
with:
5257
languages: ${{ matrix.language }}
5358
build-mode: ${{ matrix.build-mode }}
@@ -75,6 +80,6 @@ jobs:
7580
exit 1
7681
7782
- name: Perform CodeQL Analysis
78-
uses: github/codeql-action/analyze@dd903d2e4f5405488e5ef1422510ee31c8b32357 # v3
83+
uses: github/codeql-action/analyze@411c4c9a36b3fca4d674f06b6396b2c6d23522c6 # v3
7984
with:
8085
category: "/language:${{matrix.language}}"

.github/workflows/e2e.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ on:
77
merge_group:
88
types: [checks_requested]
99

10+
permissions:
11+
contents: read
12+
1013
jobs:
1114
e2e-mock:
1215
runs-on: ubuntu-latest

.github/workflows/label-pr-review-state.yml

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626
with:
2727
script: |
2828
const { owner, repo } = context.repo;
29-
const stateLabels = ['awaiting-author', 'awaiting-review'];
29+
const stateLabels = ['awaiting-author', 'awaiting-review', 'has-conflicts'];
3030
3131
// When triggered by a single PR event, only reconcile that PR.
3232
// The hourly schedule and workflow_dispatch reconcile all open PRs.
@@ -43,9 +43,29 @@ jobs:
4343
});
4444
}
4545
46+
// Only a `pull_request`/`pull_request_review` run that was itself triggered
47+
// FROM a fork gets a read-only GITHUB_TOKEN — label mutations there 403 with
48+
// "Resource not accessible by integration". `schedule` and `workflow_dispatch`
49+
// runs always execute in the base repo's context with a read/write token, even
50+
// when the PR they're reconciling happens to come from a fork, so they must NOT
51+
// be skipped or fork PRs would never get stale labels cleaned up.
52+
// See: https://docs.github.com/en/actions/concepts/security/github_token
53+
const isReadOnlyRun = Boolean(context.payload.pull_request) &&
54+
context.payload.pull_request.head?.repo?.owner?.login !== owner;
55+
56+
function isForkPR(pr) {
57+
return pr.head?.repo?.owner?.login && pr.head.repo.owner.login !== owner;
58+
}
59+
4660
// Strips stateLabels from a PR, optionally keeping one.
4761
// Also removes stale-awaiting-author when not keeping awaiting-author.
62+
// Only skipped when this run's own token is read-only (see isReadOnlyRun) —
63+
// schedule/workflow_dispatch runs reconcile fork PRs normally.
4864
async function reconcileLabels(pr, desiredLabel) {
65+
if (isReadOnlyRun && isForkPR(pr)) {
66+
core.info(`PR #${pr.number}: fork PR on a read-only run — skipping label mutation`);
67+
return;
68+
}
4969
const currentLabels = new Set(pr.labels.map(l => l.name));
5070
for (const label of stateLabels) {
5171
if (label !== desiredLabel && currentLabels.has(label)) {
@@ -105,6 +125,22 @@ jobs:
105125
continue;
106126
}
107127
128+
// `mergeable`/`mergeable_state` are only returned by the single-PR GET
129+
// endpoint, and are computed asynchronously by GitHub — a PR fetched via
130+
// pulls.list (schedule/workflow_dispatch runs) never has them, and even a
131+
// single-PR fetch can return `null`/"unknown" if the merge check hasn't
132+
// finished yet. Re-fetch the single PR to get a fresh value, and treat
133+
// "unknown" as not-yet-computed rather than as conflicting.
134+
const prDetail = prNumber
135+
? pr
136+
: (await github.rest.pulls.get({ owner, repo, pull_number: pr.number })).data;
137+
138+
if (prDetail.mergeable === false && prDetail.mergeable_state === 'dirty') {
139+
core.info(`PR #${pr.number}: has merge conflicts — labeling has-conflicts`);
140+
await reconcileLabels(pr, 'has-conflicts');
141+
continue;
142+
}
143+
108144
// Check CI status for required checks on the PR's head commit only.
109145
// Scoping to required checks avoids advisory checks (e.g. codecov/patch)
110146
// incorrectly blocking label assignment on otherwise-ready PRs.
@@ -117,9 +153,28 @@ jobs:
117153
}),
118154
]);
119155
156+
// listForRef returns every check run ever recorded on the ref, including
157+
// stale superseded ones (e.g. a failed run later re-run green). Branch
158+
// protection and the PR UI only consider the latest run per check name, so
159+
// reduce to that before evaluating — otherwise a single stale failure makes
160+
// ciFailed true forever and state labels never come back. See issue #884.
161+
//
162+
// Unlike listReviews (which documents oldest-first order), listForRef's
163+
// ordering is unspecified, so we pick the latest by run.id — GitHub assigns
164+
// monotonically increasing IDs, and id is never null (a freshly re-queued
165+
// run can have started_at: null, which would lose a string comparison
166+
// against an older completed run's timestamp).
167+
const latestByName = new Map();
168+
for (const run of checkRuns) {
169+
const prev = latestByName.get(run.name);
170+
if (!prev || run.id > prev.id) {
171+
latestByName.set(run.name, run);
172+
}
173+
}
174+
120175
// Filter to required checks only (or all checks if rules unavailable).
121176
// Always exclude this workflow's own run to avoid self-referential loops.
122-
const relevantRuns = checkRuns.filter(run => {
177+
const relevantRuns = [...latestByName.values()].filter(run => {
123178
if (run.name === 'Reconcile PR review state labels') return false;
124179
return requiredCheckNames ? requiredCheckNames.has(run.name) : true;
125180
});

.github/workflows/marketplace-publish.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ on:
66
- "v*.*.*"
77
workflow_dispatch:
88

9+
# Least privilege: publish-stable escalates to contents: write via its own
10+
# job-level permissions block.
11+
permissions:
12+
contents: read
13+
914
jobs:
1015
check-pr-approval:
1116
runs-on: ubuntu-latest

.github/workflows/nightly-publish.yml

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,21 @@ jobs:
4141
exit 1
4242
fi
4343
44+
- name: Skip if this push is a release merge
45+
id: release-check
46+
run: |
47+
commit_subject=$(git log -1 --format=%s)
48+
skip=false
49+
50+
if [[ "$commit_subject" =~ ^chore:\ prepare\ v[0-9]+\.[0-9]+\.[0-9]+\ release ]]; then
51+
echo "Commit '${commit_subject}' looks like a release-prep merge; skipping nightly pre-release."
52+
skip=true
53+
fi
54+
55+
echo "skip=${skip}" >> "$GITHUB_OUTPUT"
56+
4457
- name: Set pre-release version
58+
if: steps.release-check.outputs.skip != 'true'
4559
id: version
4660
env:
4761
RUN_NUMBER: ${{ github.run_number }}
@@ -61,13 +75,15 @@ jobs:
6175
EOF
6276
6377
- name: Build workspace packages
78+
if: steps.release-check.outputs.skip != 'true'
6479
env:
6580
PKG_RELEASE_CHANNEL: prerelease
6681
run: |
6782
pnpm --filter @roo-code/build build
6883
pnpm --filter @roo-code/vscode-webview build
6984
7085
- name: Package pre-release VSIX
86+
if: steps.release-check.outputs.skip != 'true'
7187
env:
7288
POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }}
7389
PKG_RELEASE_CHANNEL: prerelease
@@ -76,6 +92,7 @@ jobs:
7692
pnpm --filter ./src exec vsce package --pre-release --no-dependencies --out ../bin
7793
7894
- name: Verify VSIX contents
95+
if: steps.release-check.outputs.skip != 'true'
7996
env:
8097
VERSION_NUMBER: ${{ steps.version.outputs.number }}
8198
run: |
@@ -87,6 +104,7 @@ jobs:
87104
grep -q "extension/webview-ui/audio/celebration.wav" /tmp/zoo-code-vsix-contents.txt
88105
89106
- name: Validate packaged manifest identity
107+
if: steps.release-check.outputs.skip != 'true'
90108
env:
91109
VERSION_NUMBER: ${{ steps.version.outputs.number }}
92110
run: |
@@ -98,12 +116,36 @@ jobs:
98116
test "$artifact_name" = "zoo-code"
99117
test "$artifact_publisher" = "ZooCodeOrganization"
100118
101-
# Open VSX is intentionally excluded: it has no pre-release channel concept,
102-
# so pre-release builds would surface as the latest stable version for all users.
103119
- name: Publish pre-release to VS Code Marketplace
120+
if: steps.release-check.outputs.skip != 'true'
104121
env:
105122
VSCE_PAT: ${{ secrets.VSCE_PAT }}
106123
VERSION_NUMBER: ${{ steps.version.outputs.number }}
107124
run: |
108-
npx @vscode/vsce publish --pre-release --packagePath "bin/zoo-code-${VERSION_NUMBER}.vsix"
109-
echo "Published ZooCodeOrganization.zoo-code ${VERSION_NUMBER} as a VS Code Marketplace pre-release"
125+
npx @vscode/vsce publish --pre-release --skip-duplicate --packagePath "bin/zoo-code-${VERSION_NUMBER}.vsix"
126+
echo "Published or skipped existing ZooCodeOrganization.zoo-code ${VERSION_NUMBER} as a VS Code Marketplace pre-release"
127+
128+
# The VSIX built above with `vsce package --pre-release` already carries the
129+
# Microsoft.VisualStudio.Code.PreRelease manifest property, which is what Open
130+
# VSX reads to flag the version. `ovsx publish` ignores --pre-release for an
131+
# already-packaged .vsix (it only applies when ovsx does the packaging itself),
132+
# so it's intentionally omitted here.
133+
#
134+
# Open VSX's "latest" alias resolves to the highest semver across stable and
135+
# pre-release alike (pre-release only breaks ties at equal major.minor.patch),
136+
# so a nightly build can transiently become "latest" until the next stable
137+
# release outranks it. This mirrors how Marketplace pre-release users already
138+
# track the newest published version, so it's an accepted trade-off here too.
139+
- name: Publish pre-release to Open VSX Registry
140+
if: steps.release-check.outputs.skip != 'true'
141+
env:
142+
OVSX_PAT: ${{ secrets.OVSX_PAT }}
143+
VERSION_NUMBER: ${{ steps.version.outputs.number }}
144+
run: |
145+
set -o pipefail
146+
publish_output=$(pnpm exec ovsx publish "bin/zoo-code-${VERSION_NUMBER}.vsix" --skip-duplicate 2>&1 | tee /dev/stderr)
147+
if echo "$publish_output" | grep -q "is already published"; then
148+
echo "ZooCodeOrganization.zoo-code ${VERSION_NUMBER} was already published to Open VSX; skipped."
149+
else
150+
echo "Published ZooCodeOrganization.zoo-code ${VERSION_NUMBER} as an Open VSX pre-release"
151+
fi
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
name: Post weekly release reminder
2+
3+
# Requires repo secret DISCORD_RELEASE_WEBHOOK_URL — Discord webhook for the
4+
# release-reminders channel. The reminder job fails (noisily) if it is unset.
5+
6+
on:
7+
schedule:
8+
# Friday at 09:00 UTC
9+
- cron: "0 9 * * 5"
10+
workflow_dispatch:
11+
12+
permissions: {}
13+
14+
concurrency:
15+
group: release-reminder
16+
# A re-run produces an identical message, so cancel a stale/duplicate run
17+
# rather than risk posting twice.
18+
cancel-in-progress: true
19+
20+
jobs:
21+
remind:
22+
runs-on: ubuntu-latest
23+
steps:
24+
- name: Post release reminder to Discord
25+
env:
26+
DISCORD_RELEASE_WEBHOOK_URL: ${{ secrets.DISCORD_RELEASE_WEBHOOK_URL }}
27+
run: |
28+
set -euo pipefail
29+
owners=("Elliott" "Navad" "Toray")
30+
anchor_date="2026-07-17"
31+
seconds_since_anchor=$(($(date -u +%s) - $(date -u -d "$anchor_date" +%s)))
32+
weeks_since_anchor=$((seconds_since_anchor / 604800))
33+
owner="${owners[$((weeks_since_anchor % ${#owners[@]}))]}"
34+
35+
message=$(cat <<EOF
36+
**Weekly release reminder**
37+
38+
**${owner}** is on deck for this week's Zoo Code release.
39+
40+
1. Create the release branch and prepare the release with \`.roo/commands/release.md\`.
41+
2. Get the release PR approved.
42+
3. Tag the release branch to trigger the Marketplace publish workflow.
43+
4. Approve the deployment when the \`marketplace-production\` environment requests it.
44+
5. After publishing succeeds, add the release PR to the merge queue.
45+
46+
Avoid merging or rebasing the release branch before publishing, so the approved tag keeps the intended commit SHA.
47+
48+
More detail: https://discord.com/channels/1497384592494297201/1498024845945081999/1512914847124291667
49+
EOF
50+
)
51+
52+
payload=$(jq -n --arg content "$message" '{content: $content}')
53+
curl --fail-with-body --silent --show-error \
54+
--header 'Content-Type: application/json' \
55+
--data "$payload" \
56+
"$DISCORD_RELEASE_WEBHOOK_URL"
57+
58+
- name: Notify on failure
59+
if: failure()
60+
env:
61+
DISCORD_RELEASE_WEBHOOK_URL: ${{ secrets.DISCORD_RELEASE_WEBHOOK_URL }}
62+
run: |
63+
set -euo pipefail
64+
payload=$(jq -nc --arg content "⚠️ release-reminder workflow failed — see the Actions runs." '{content: $content}')
65+
curl --fail-with-body --silent --show-error \
66+
--header 'Content-Type: application/json' \
67+
--data "$payload" \
68+
"$DISCORD_RELEASE_WEBHOOK_URL" || true

.github/workflows/release-validation.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ on:
1616
- "releases/**"
1717
- ".github/workflows/release-validation.yml"
1818

19+
permissions:
20+
contents: read
21+
1922
jobs:
2023
validate-release:
2124
runs-on: ubuntu-latest

0 commit comments

Comments
 (0)