mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-01 20:29:00 +00:00
fix: allow stable promotion of published beta history
Issue: #130 User-Visible: no
This commit is contained in:
+1
-1
@@ -58,7 +58,7 @@ while read -r local_ref local_sha remote_ref remote_sha; do
|
||||
|
||||
echo "process-gate: $local_ref, диапазон ${base}..${local_sha}" >&2
|
||||
# shellcheck disable=SC2086
|
||||
if ! node "$gate" --range "${base}..${local_sha}" $issues_flag >&2; then
|
||||
if ! node "$gate" --range "${base}..${local_sha}" --target-ref "$remote_ref" $issues_flag >&2; then
|
||||
status=1
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -51,6 +51,7 @@ jobs:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
HEAD_SHA: ${{ github.sha }}
|
||||
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
||||
TARGET_REF: ${{ github.ref }}
|
||||
# Публичный репозиторий: штатного токена хватает на чтение issue.
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
|
||||
+8
-1
@@ -608,7 +608,7 @@ Project v2 остаётся человеческим представление
|
||||
{`S5-ready`, `S6-in-progress`, `S7-code-review`, `S8-merged`}; закрытый,
|
||||
недоступный или помеченный `blocked` — отказ (**fail closed**).
|
||||
|
||||
Две оговорки к проверке 8, обе выяснились при реализации.
|
||||
Три оговорки к проверке 8 выяснились при реализации.
|
||||
|
||||
**`S8-merged` входит в множество**, хотя по смыслу задача уже принята. Причина
|
||||
механическая: конвейер (§10.4) сливает ветку в `dev` **раньше**, чем ставит метку,
|
||||
@@ -621,6 +621,13 @@ Validate стартует от этого push и успевает прочит
|
||||
документ ревью: он ложится в ветку задачи, пока та в `S4-spec-review` или
|
||||
`S7-code-review`, то есть заведомо вне рабочего множества.
|
||||
|
||||
**При продвижении в `main` не перепроверяются коммиты, уже достижимые из
|
||||
prerelease-тега.** После выпуска беты их issue по §2.8 должны быть закрыты, а
|
||||
stable fast-forward снова включает эти коммиты в диапазон `old-main..candidate`.
|
||||
Pre-push передаёт целевую remote ref через `--target-ref`, а Validate — через
|
||||
`TARGET_REF`; оба исключают только уже опубликованную prerelease-историю. Любой
|
||||
post-beta коммит остаётся в проверке и по закрытому issue отклоняется fail-closed.
|
||||
|
||||
Не реализовано и остаётся долгом:
|
||||
|
||||
9. `npm run release:prerelease -- --issues=…` не проверяет, есть ли у issue
|
||||
|
||||
@@ -289,6 +289,22 @@ export function commitsUnderRuleOne(commits) {
|
||||
);
|
||||
}
|
||||
|
||||
export function isStableTarget(targetRef) {
|
||||
return /^(?:refs\/heads\/)?main$/.test(targetRef ?? '');
|
||||
}
|
||||
|
||||
// При stable promotion диапазон main..candidate закономерно содержит коммиты,
|
||||
// уже выпущенные prerelease-тегом. Их issue к этому моменту обязаны быть закрыты
|
||||
// (§2.8), поэтому повторная online-проверка статуса дала бы ложный отказ. Новые
|
||||
// post-beta коммиты остаются в выборке и проверяются fail-closed как обычно.
|
||||
export function commitsNeedingIssueStatus(
|
||||
commits, { targetRef = '', isPublishedPrereleaseCommit = () => false } = {},
|
||||
) {
|
||||
const underRuleOne = commitsUnderRuleOne(commits);
|
||||
if (!isStableTarget(targetRef)) return underRuleOne;
|
||||
return underRuleOne.filter((commit) => !isPublishedPrereleaseCommit(commit.sha));
|
||||
}
|
||||
|
||||
// 8. статус issue. Fail closed: недоступный или закрытый issue — отказ, а не
|
||||
// пропуск. Гейт, который молчит при недоступном источнике правды, бесполезен.
|
||||
export function checkIssueStatuses(numbers, runner, { allowed = ALLOWED_STATUS } = {}) {
|
||||
@@ -382,6 +398,7 @@ function main(argv) {
|
||||
|
||||
const repo = value('repo', process.cwd());
|
||||
const allowed = flag('no-merged') ? STRICT_STATUS : ALLOWED_STATUS;
|
||||
const targetRef = value('target-ref', process.env.TARGET_REF ?? '');
|
||||
|
||||
let range = value('range');
|
||||
if (!range && flag('github-range')) {
|
||||
@@ -428,7 +445,23 @@ function main(argv) {
|
||||
// проверки 3. Второй запрос по тому же issue — лишний сетевой вызов.
|
||||
let labelsOf = null;
|
||||
if (flag('issues')) {
|
||||
const numbers = [...new Set(commitsUnderRuleOne(commits).flatMap((c) => c.issues).map((t) => t.slice(1)))];
|
||||
const prereleaseTags = isStableTarget(targetRef)
|
||||
? git(['tag', '--list'], repo).split('\n').map((s) => s.trim()).filter((tag) =>
|
||||
/^v(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)-[0-9A-Za-z.-]+$/.test(tag))
|
||||
: [];
|
||||
const publishedCache = new Map();
|
||||
const isPublishedPrereleaseCommit = (sha) => {
|
||||
if (!publishedCache.has(sha)) {
|
||||
publishedCache.set(sha, prereleaseTags.some((tag) =>
|
||||
spawnSync('git', ['-C', repo, 'merge-base', '--is-ancestor', sha, `${tag}^{commit}`],
|
||||
{ encoding: 'utf8' }).status === 0));
|
||||
}
|
||||
return publishedCache.get(sha);
|
||||
};
|
||||
const statusCommits = commitsNeedingIssueStatus(commits, {
|
||||
targetRef, isPublishedPrereleaseCommit,
|
||||
});
|
||||
const numbers = [...new Set(statusCommits.flatMap((c) => c.issues).map((t) => t.slice(1)))];
|
||||
const runner = ghRunner(process.env.HP_REPO ?? 'Matysh/houseplan-card', process.env.GH_BIN ?? 'gh');
|
||||
const cache = new Map();
|
||||
const cached = (nn) => {
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
checkReviewDocLimit,
|
||||
checkSpecs,
|
||||
classify,
|
||||
commitsNeedingIssueStatus,
|
||||
commitsUnderRuleOne,
|
||||
evaluateCommit,
|
||||
makeCommit,
|
||||
@@ -170,6 +171,36 @@ test('only class A/B commits are held to the issue status', () => {
|
||||
assert.deepEqual(commitsUnderRuleOne([reviewDoc]), []);
|
||||
});
|
||||
|
||||
test('stable promotion skips status recheck only for commits already published in a prerelease', () => {
|
||||
const published = makeCommit({
|
||||
sha: 'a'.repeat(40), subject: 'Fix shipped in beta', body: 'Issue: #123', files: ['src/a.ts'],
|
||||
});
|
||||
const postBeta = makeCommit({
|
||||
sha: 'b'.repeat(40), subject: 'New promotion work', body: 'Issue: #130', files: ['scripts/a.mjs'],
|
||||
});
|
||||
const publishedShas = new Set([published.sha]);
|
||||
assert.deepEqual(
|
||||
commitsNeedingIssueStatus([published, postBeta], {
|
||||
targetRef: 'refs/heads/main',
|
||||
isPublishedPrereleaseCommit: (sha) => publishedShas.has(sha),
|
||||
}).map((commit) => commit.sha),
|
||||
[postBeta.sha],
|
||||
);
|
||||
assert.deepEqual(
|
||||
rules(checkIssueStatuses(['130'], () => ({
|
||||
ok: true, json: { state: 'CLOSED', labels: [] },
|
||||
}))),
|
||||
[8],
|
||||
);
|
||||
assert.deepEqual(
|
||||
commitsNeedingIssueStatus([published, postBeta], {
|
||||
targetRef: 'refs/heads/dev',
|
||||
isPublishedPrereleaseCommit: (sha) => publishedShas.has(sha),
|
||||
}).map((commit) => commit.sha),
|
||||
[published.sha, postBeta.sha],
|
||||
);
|
||||
});
|
||||
|
||||
test('issue status check is fail closed when the source of truth is unreachable', () => {
|
||||
// AC3: недоступный gh должен давать отказ, а не молчаливый пропуск.
|
||||
const broken = () => ({ ok: false, error: 'gh: could not resolve to a Repository' });
|
||||
|
||||
Reference in New Issue
Block a user