mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-28 13:32:23 +00:00
fix release workflow limits and branch selection (#4866)
This commit is contained in:
@@ -15,11 +15,11 @@ import {
|
||||
test('release notes args default to the latest GitHub release as the start ref', () => {
|
||||
assert.equal(parseArgs([]).from, 'latest-release');
|
||||
|
||||
const parsed = parseArgs(['--from', 'v1.0.0', '--to', 'main', '--no-ai', '--max-prs', '5']);
|
||||
const parsed = parseArgs(['--from', 'v1.0.0', '--to', 'main', '--no-ai']);
|
||||
assert.equal(parsed.from, 'v1.0.0');
|
||||
assert.equal(parsed.to, 'main');
|
||||
assert.equal(parsed.noAi, true);
|
||||
assert.equal(parsed.maxPrs, 5);
|
||||
assert.equal(parsed.maxPrs, undefined);
|
||||
});
|
||||
|
||||
test('GitHub repo is inferred from ssh and https remotes', () => {
|
||||
@@ -60,6 +60,31 @@ test('OpenAI request contains required release sections and compare payload', ()
|
||||
assert.match(request.input[1].content, /https:\/\/github\.com\/tinyhumansai\/openhuman\/compare\/v1\.0\.0\.\.\.main/);
|
||||
});
|
||||
|
||||
test('OpenAI request compacts release ranges larger than the prompt budget', () => {
|
||||
const pullRequests = Array.from({ length: 300 }, (_, index) => ({
|
||||
number: index + 1,
|
||||
title: `Release improvement ${index + 1}`,
|
||||
url: `https://github.com/tinyhumansai/openhuman/pull/${index + 1}`,
|
||||
author: `contributor-${index + 1}`,
|
||||
labels: ['enhancement'],
|
||||
body: 'x'.repeat(700),
|
||||
commits: [{ sha: `${index + 1}`, subject: `Release improvement ${index + 1}` }],
|
||||
}));
|
||||
const payload = buildReleasePayload({
|
||||
from: 'v1.0.0',
|
||||
to: 'main',
|
||||
resolvedTo: 'main',
|
||||
repo: 'tinyhumansai/openhuman',
|
||||
commits: [],
|
||||
contributors: [],
|
||||
pullRequests,
|
||||
});
|
||||
|
||||
const request = buildOpenAiRequest({ model: 'gpt-5.2', title: 'Large release', payload });
|
||||
assert.match(request.input[1].content, /Release improvement 300/);
|
||||
assert.doesNotMatch(request.input[1].content, /x{700}/);
|
||||
});
|
||||
|
||||
test('release payload omits contributor emails before AI summarization', () => {
|
||||
const payload = buildReleasePayload({
|
||||
from: 'v1.0.0',
|
||||
|
||||
@@ -24,7 +24,6 @@ Options:
|
||||
--output <file> Write generated Markdown to a file instead of stdout.
|
||||
--no-ai Build deterministic Markdown without calling OpenAI.
|
||||
--dry-run Print the OpenAI prompt/input JSON without calling OpenAI.
|
||||
--max-prs <n> Refuse ranges with more than n PRs. Defaults to 250.
|
||||
--help Show this help.
|
||||
|
||||
Environment:
|
||||
@@ -52,7 +51,6 @@ export function parseArgs(argv) {
|
||||
output: null,
|
||||
noAi: false,
|
||||
dryRun: false,
|
||||
maxPrs: 250,
|
||||
};
|
||||
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
@@ -79,12 +77,6 @@ export function parseArgs(argv) {
|
||||
options.model = readValue(arg);
|
||||
} else if (arg === '--output' || arg === '-o') {
|
||||
options.output = readValue(arg);
|
||||
} else if (arg === '--max-prs') {
|
||||
const parsed = Number.parseInt(readValue(arg), 10);
|
||||
if (!Number.isInteger(parsed) || parsed < 1) {
|
||||
throw new Error('--max-prs must be a positive integer');
|
||||
}
|
||||
options.maxPrs = parsed;
|
||||
} else if (arg === '--no-ai') {
|
||||
options.noAi = true;
|
||||
} else if (arg === '--dry-run') {
|
||||
@@ -295,12 +287,9 @@ function fetchPullRequest(repo, number) {
|
||||
}
|
||||
}
|
||||
|
||||
function collectPullRequests(repo, commits, maxPrs) {
|
||||
function collectPullRequests(repo, commits) {
|
||||
const prCommits = collectPrCommits(commits);
|
||||
const numbers = [...prCommits.keys()].sort((a, b) => a - b);
|
||||
if (numbers.length > maxPrs) {
|
||||
throw new Error(`Range contains ${numbers.length} PRs, above --max-prs ${maxPrs}`);
|
||||
}
|
||||
|
||||
return numbers.map((number) => {
|
||||
const detail = fetchPullRequest(repo, number);
|
||||
@@ -373,12 +362,7 @@ export function buildReleasePayload({ from, to, resolvedTo, repo, commits, pullR
|
||||
}
|
||||
|
||||
export function buildOpenAiRequest({ model, title, payload }) {
|
||||
const compactPayload = JSON.stringify(payload);
|
||||
if (compactPayload.length > MAX_PROMPT_CHARS) {
|
||||
throw new Error(
|
||||
`Release payload is ${compactPayload.length} characters, above ${MAX_PROMPT_CHARS}. Use a narrower range.`,
|
||||
);
|
||||
}
|
||||
const compactPayload = serializeOpenAiPayload(payload);
|
||||
|
||||
return {
|
||||
model,
|
||||
@@ -413,6 +397,49 @@ ${compactPayload}`,
|
||||
};
|
||||
}
|
||||
|
||||
function serializeOpenAiPayload(payload) {
|
||||
const fullPayload = JSON.stringify(payload);
|
||||
if (fullPayload.length <= MAX_PROMPT_CHARS) {
|
||||
return fullPayload;
|
||||
}
|
||||
|
||||
const compact = {
|
||||
...payload,
|
||||
contributors: payload.contributors.map(({ name, isNew }) => ({ name, isNew })),
|
||||
pullRequests: payload.pullRequests.map(({ number, title, url, author, labels }) => ({
|
||||
number,
|
||||
title,
|
||||
url,
|
||||
author,
|
||||
labels,
|
||||
})),
|
||||
uncategorizedCommits: payload.uncategorizedCommits.map(({ subject, authorName }) => ({
|
||||
subject,
|
||||
authorName,
|
||||
})),
|
||||
};
|
||||
const compactPayload = JSON.stringify(compact);
|
||||
if (compactPayload.length <= MAX_PROMPT_CHARS) {
|
||||
return compactPayload;
|
||||
}
|
||||
|
||||
const bounded = {
|
||||
...compact,
|
||||
pullRequests: [],
|
||||
omittedPullRequests: compact.pullRequests.length,
|
||||
};
|
||||
for (const pullRequest of compact.pullRequests) {
|
||||
bounded.pullRequests.push(pullRequest);
|
||||
bounded.omittedPullRequests -= 1;
|
||||
if (JSON.stringify(bounded).length > MAX_PROMPT_CHARS) {
|
||||
bounded.pullRequests.pop();
|
||||
bounded.omittedPullRequests += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return JSON.stringify(bounded);
|
||||
}
|
||||
|
||||
function getOpenAiKey() {
|
||||
if (process.env.OPENAI_API_KEY) {
|
||||
return process.env.OPENAI_API_KEY;
|
||||
@@ -646,7 +673,7 @@ async function main() {
|
||||
console.error(`[release-notes] Collecting ${repo} changes from ${from} to ${resolvedTo}`);
|
||||
const commits = collectCommits(from, resolvedTo);
|
||||
const contributors = collectContributorStats(commits, priorAuthorKeys(from));
|
||||
const pullRequests = collectPullRequests(repo, commits, options.maxPrs);
|
||||
const pullRequests = collectPullRequests(repo, commits);
|
||||
const payload = buildReleasePayload({
|
||||
from,
|
||||
to: options.to,
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Require a successful "CI Full Gate" check run before cutting a release.
|
||||
#
|
||||
# Usage: require-ci-full-gate.sh <sha>
|
||||
#
|
||||
# Called by release-staging.yml / release-production.yml on the commit they
|
||||
# are about to bump/tag/build. Direct App-token pushes bypass the PR merge
|
||||
# gate, so without this check a maintainer could cut a release while CI Full
|
||||
# is still pending or after it failed on release HEAD.
|
||||
#
|
||||
# Version-bump commits carry [skip ci] and therefore never get a CI Full run,
|
||||
# so first-parent ancestry is walked past them (bounded) to the most recent
|
||||
# commit that should have one. Needs GH_TOKEN with checks:read and
|
||||
# GITHUB_REPOSITORY set (both standard in Actions).
|
||||
set -euo pipefail
|
||||
|
||||
SHA="${1:?usage: require-ci-full-gate.sh <sha>}"
|
||||
CHECK_NAME="CI Full Gate"
|
||||
MAX_SKIP_DEPTH=10
|
||||
|
||||
log() { echo "[release][ci-full-gate] $*"; }
|
||||
|
||||
target="$SHA"
|
||||
depth=0
|
||||
while [ "$depth" -le "$MAX_SKIP_DEPTH" ]; do
|
||||
subject="$(git log -1 --format=%s "$target")"
|
||||
if [[ "$subject" != *"[skip ci]"* ]]; then
|
||||
break
|
||||
fi
|
||||
log "$target is a [skip ci] commit ('$subject') — checking its first parent instead"
|
||||
target="$(git rev-parse "${target}^")"
|
||||
depth=$((depth + 1))
|
||||
done
|
||||
if [ "$depth" -gt "$MAX_SKIP_DEPTH" ]; then
|
||||
echo "::error::Walked ${MAX_SKIP_DEPTH} [skip ci] commits from ${SHA} without finding a CI-validated commit — something is off with the release history."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "requiring a successful '${CHECK_NAME}' check run on ${target}"
|
||||
result="$(gh api -X GET \
|
||||
"repos/${GITHUB_REPOSITORY}/commits/${target}/check-runs" \
|
||||
-f check_name="${CHECK_NAME}" -f filter=latest \
|
||||
--jq '[(.check_runs[0].status // "missing"), (.check_runs[0].conclusion // "none")] | join(" ")')"
|
||||
status="${result%% *}"
|
||||
conclusion="${result##* }"
|
||||
log "status=${status} conclusion=${conclusion}"
|
||||
|
||||
if [ "${status}" != "completed" ] || [ "${conclusion}" != "success" ]; then
|
||||
echo "::error::'${CHECK_NAME}' on ${target} is ${status}/${conclusion} — cut releases only from a commit with a green CI Full run (re-run ci-full.yml if needed). The skip_ci_gate input overrides this for operator recovery only."
|
||||
exit 1
|
||||
fi
|
||||
log "'${CHECK_NAME}' is green on ${target}"
|
||||
Reference in New Issue
Block a user