fix(release): enforce August 1 business readiness
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import test from 'node:test'
|
||||
|
||||
const testDir = path.dirname(fileURLToPath(import.meta.url))
|
||||
const projectRoot = path.resolve(testDir, '..', '..')
|
||||
const verifierPath = path.join(projectRoot, 'scripts', 'verify-aug1-formal-content.mjs')
|
||||
const pendingApprovalPath = path.join(
|
||||
projectRoot,
|
||||
'docs',
|
||||
'content-candidates',
|
||||
'aug1-release-approval.json',
|
||||
)
|
||||
const candidatePath = path.join(
|
||||
projectRoot,
|
||||
'docs',
|
||||
'content-candidates',
|
||||
'aug1-life-advisor-content-candidates-v0.1.json',
|
||||
)
|
||||
const candidateBytes = await readFile(candidatePath)
|
||||
const candidate = JSON.parse(candidateBytes.toString('utf8'))
|
||||
const candidateSha256 = createHash('sha256').update(candidateBytes).digest('hex')
|
||||
|
||||
const source = {
|
||||
sourceId: 'FORMAL-OPS-001',
|
||||
title: '现行生活顾问服务作业标准',
|
||||
version: '2026.07',
|
||||
effectiveDate: '2026-07-01',
|
||||
scope: '受控内测项目',
|
||||
sha256: 'a'.repeat(64),
|
||||
formalPolicy: true,
|
||||
}
|
||||
|
||||
function validApproval() {
|
||||
const scenarioApprovals = Object.fromEntries(candidate.scenarios.map((item, index) => {
|
||||
const highRisk = item.proposedRiskLevel === '高风险'
|
||||
return [item.candidateId, {
|
||||
status: 'APPROVED',
|
||||
scenarioCode: item.scenarioDraft.id,
|
||||
riskLevel: item.proposedRiskLevel,
|
||||
sourceRefs: ['FORMAL-OPS-001:section-1'],
|
||||
reviewedBy: `reviewer-${index + 1}`,
|
||||
reviewedAt: '2026-07-30T09:00:00+08:00',
|
||||
...(highRisk ? {
|
||||
secondReviewedBy: `second-reviewer-${index + 1}`,
|
||||
secondReviewedAt: '2026-07-30T10:00:00+08:00',
|
||||
} : {}),
|
||||
businessEvidenceRef: `evidence/scenario-${index + 1}.pdf`,
|
||||
productionContentVersion: `aug1-v${index + 1}`,
|
||||
productionContentHash: String(index + 1).repeat(64),
|
||||
}]
|
||||
}))
|
||||
const policyQuestionApprovals = Object.fromEntries(candidate.policyQuestionCandidates.map((item, index) => {
|
||||
const answerDisposition = item.category === 'NO_EVIDENCE'
|
||||
? 'FORMAL_NO_EVIDENCE_BOUNDARY'
|
||||
: item.category === 'UNAUTHORIZED'
|
||||
? 'FORMAL_UNAUTHORIZED_BOUNDARY'
|
||||
: 'FORMALLY_SOURCED'
|
||||
return [item.id, {
|
||||
status: 'APPROVED',
|
||||
answerDisposition,
|
||||
sourceRefs: ['FORMAL-OPS-001:section-2'],
|
||||
expectedBehavior: item.expectedBehavior,
|
||||
observedBehavior: '验收结果与预期边界一致',
|
||||
evidenceRef: `evidence/question-${index + 1}.json`,
|
||||
evidenceSha256: 'b'.repeat(64),
|
||||
reviewedBy: `question-reviewer-${index + 1}`,
|
||||
reviewedAt: '2026-07-30T11:00:00+08:00',
|
||||
}]
|
||||
}))
|
||||
const signoffs = Object.fromEntries([
|
||||
'businessOwner',
|
||||
'knowledgeOwner',
|
||||
'trainingOwner',
|
||||
'channelOwner',
|
||||
'releaseOwner',
|
||||
].map((role) => [role, {
|
||||
status: 'APPROVED',
|
||||
reviewedBy: `${role}-reviewer`,
|
||||
reviewedAt: '2026-07-30T12:00:00+08:00',
|
||||
}]))
|
||||
return {
|
||||
schemaVersion: '1.0',
|
||||
releaseTarget: '2026-08-01',
|
||||
tenantId: '000000',
|
||||
candidateFile: 'aug1-life-advisor-content-candidates-v0.1.json',
|
||||
candidateSha256,
|
||||
releaseStatus: 'APPROVED',
|
||||
formalContentVersion: 'aug1-formal-v1',
|
||||
sourceRegistry: [source],
|
||||
scenarioApprovals,
|
||||
policyQuestionApprovals,
|
||||
signoffs,
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyApproval(approval, ...options) {
|
||||
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'aug1-formal-'))
|
||||
const tempCandidatePath = path.join(tempDir, 'aug1-life-advisor-content-candidates-v0.1.json')
|
||||
const tempApprovalPath = path.join(tempDir, 'approval.json')
|
||||
try {
|
||||
await writeFile(tempCandidatePath, candidateBytes)
|
||||
await writeFile(tempApprovalPath, JSON.stringify(approval), 'utf8')
|
||||
return spawnSync(process.execPath, [verifierPath, ...options, tempApprovalPath], {
|
||||
cwd: projectRoot,
|
||||
encoding: 'utf8',
|
||||
})
|
||||
} finally {
|
||||
await rm(tempDir, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
test('audits the checked-in pending manifest without claiming release readiness', () => {
|
||||
const audit = spawnSync(process.execPath, [verifierPath, pendingApprovalPath], {
|
||||
cwd: projectRoot,
|
||||
encoding: 'utf8',
|
||||
})
|
||||
assert.equal(audit.status, 0, audit.stderr)
|
||||
assert.match(audit.stdout, /approved scenarios: 0\/5/)
|
||||
assert.match(audit.stdout, /approved policy questions: 0\/30/)
|
||||
|
||||
const strict = spawnSync(process.execPath, [verifierPath, '--strict', pendingApprovalPath], {
|
||||
cwd: projectRoot,
|
||||
encoding: 'utf8',
|
||||
})
|
||||
assert.notEqual(strict.status, 0)
|
||||
assert.match(strict.stderr, /releaseStatus must be APPROVED/)
|
||||
})
|
||||
|
||||
test('accepts complete formal evidence and emits five immutable production snapshots', async () => {
|
||||
const strict = await verifyApproval(validApproval(), '--strict')
|
||||
assert.equal(strict.status, 0, strict.stderr)
|
||||
assert.match(strict.stdout, /strict release ready: true/)
|
||||
|
||||
const snapshots = await verifyApproval(validApproval(), '--scenario-snapshots')
|
||||
assert.equal(snapshots.status, 0, snapshots.stderr)
|
||||
const lines = snapshots.stdout.trim().split(/\r?\n/)
|
||||
assert.equal(lines.length, 5)
|
||||
assert.match(lines[0], /^[a-z0-9-]+\|aug1-v1\|1{64}$/)
|
||||
})
|
||||
|
||||
test('rejects incomplete or contradictory formal approvals', async (t) => {
|
||||
const cases = [
|
||||
['missing question', (approval) => {
|
||||
delete approval.policyQuestionApprovals[candidate.policyQuestionCandidates[0].id]
|
||||
}, /question approvals must cover exactly 30\/30/],
|
||||
['wrong no-evidence behavior', (approval) => {
|
||||
const item = candidate.policyQuestionCandidates.find((question) => question.category === 'NO_EVIDENCE')
|
||||
approval.policyQuestionApprovals[item.id].answerDisposition = 'FORMALLY_SOURCED'
|
||||
}, /must preserve the formal refusal boundary/],
|
||||
['same high-risk reviewer', (approval) => {
|
||||
const item = candidate.scenarios.find((scenario) => scenario.proposedRiskLevel === '高风险')
|
||||
approval.scenarioApprovals[item.candidateId].secondReviewedBy =
|
||||
approval.scenarioApprovals[item.candidateId].reviewedBy
|
||||
}, /high-risk reviewers must be different people/],
|
||||
['unknown source', (approval) => {
|
||||
const item = candidate.scenarios[0]
|
||||
approval.scenarioApprovals[item.candidateId].sourceRefs = ['MISSING-SOURCE:section-1']
|
||||
}, /is not in sourceRegistry/],
|
||||
['placeholder source version', (approval) => {
|
||||
approval.sourceRegistry[0].version = '待填写'
|
||||
}, /formal source version is required/],
|
||||
['missing owner sign-off', (approval) => {
|
||||
approval.signoffs.releaseOwner.status = 'PENDING'
|
||||
}, /releaseOwner: sign-off must be APPROVED/],
|
||||
]
|
||||
|
||||
for (const [name, mutate, expected] of cases) {
|
||||
await t.test(name, async () => {
|
||||
const approval = validApproval()
|
||||
mutate(approval)
|
||||
const result = await verifyApproval(approval, '--strict')
|
||||
assert.notEqual(result.status, 0)
|
||||
assert.match(result.stderr, expected)
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,222 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
SCRIPT="$ROOT_DIR/scripts/verify-aug1-release-readiness.sh"
|
||||
PREFLIGHT="$ROOT_DIR/scripts/release-preflight.sh"
|
||||
DEV_SETUP="$ROOT_DIR/docs/DEV_SETUP.md"
|
||||
|
||||
bash -n "$SCRIPT"
|
||||
bash -n "$PREFLIGHT"
|
||||
|
||||
pending_output="$("$SCRIPT" --execute 2>&1 || true)"
|
||||
grep -Fq 'releaseStatus must be APPROVED' <<<"$pending_output"
|
||||
grep -Fq 'strict approval did not return exactly five scenario snapshots' <<<"$pending_output"
|
||||
|
||||
test_tmp_base="${TMPDIR:-/tmp}"
|
||||
test_tmp="$(mktemp -d "$test_tmp_base/aug1-readiness-test.XXXXXX")"
|
||||
cleanup() {
|
||||
case "$test_tmp" in
|
||||
"$test_tmp_base"/aug1-readiness-test.*) rm -rf -- "$test_tmp" ;;
|
||||
*) echo "FAIL: unsafe test cleanup path: $test_tmp" >&2; exit 1 ;;
|
||||
esac
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
cp "$ROOT_DIR/docs/content-candidates/aug1-life-advisor-content-candidates-v0.1.json" \
|
||||
"$test_tmp/aug1-life-advisor-content-candidates-v0.1.json"
|
||||
node - "$test_tmp/aug1-life-advisor-content-candidates-v0.1.json" \
|
||||
"$test_tmp/approval.json" "$test_tmp/scenario-rows.tsv" <<'NODE'
|
||||
const fs = require('node:fs')
|
||||
const crypto = require('node:crypto')
|
||||
const [candidatePath, approvalPath, rowsPath] = process.argv.slice(2)
|
||||
const bytes = fs.readFileSync(candidatePath)
|
||||
const candidate = JSON.parse(bytes)
|
||||
const source = {
|
||||
sourceId: 'FORMAL-OPS-001',
|
||||
title: 'test-only formal operations standard',
|
||||
version: 'test-v1',
|
||||
effectiveDate: '2026-07-01',
|
||||
scope: 'test-only',
|
||||
sha256: 'a'.repeat(64),
|
||||
formalPolicy: true,
|
||||
}
|
||||
const scenarioApprovals = {}
|
||||
const rows = []
|
||||
candidate.scenarios.forEach((item, index) => {
|
||||
const highRisk = item.proposedRiskLevel === '高风险'
|
||||
const version = `aug1-v${index + 1}`
|
||||
const hash = String(index + 1).repeat(64)
|
||||
scenarioApprovals[item.candidateId] = {
|
||||
status: 'APPROVED',
|
||||
scenarioCode: item.scenarioDraft.id,
|
||||
riskLevel: item.proposedRiskLevel,
|
||||
sourceRefs: ['FORMAL-OPS-001:section-1'],
|
||||
reviewedBy: `reviewer-${index + 1}`,
|
||||
reviewedAt: '2026-07-30T09:00:00+08:00',
|
||||
...(highRisk ? {
|
||||
secondReviewedBy: `second-reviewer-${index + 1}`,
|
||||
secondReviewedAt: '2026-07-30T10:00:00+08:00',
|
||||
} : {}),
|
||||
businessEvidenceRef: `evidence/scenario-${index + 1}.pdf`,
|
||||
productionContentVersion: version,
|
||||
productionContentHash: hash,
|
||||
}
|
||||
rows.push([
|
||||
item.scenarioDraft.id,
|
||||
version,
|
||||
hash,
|
||||
'1',
|
||||
'已发布',
|
||||
item.proposedRiskLevel,
|
||||
String(100 + index),
|
||||
'2026-07-30T09:00:00',
|
||||
highRisk ? String(200 + index) : '0',
|
||||
highRisk ? '2026-07-30T10:00:00' : '-',
|
||||
'FORMAL-OPS-001:section-1',
|
||||
].join('\t'))
|
||||
})
|
||||
const policyQuestionApprovals = {}
|
||||
candidate.policyQuestionCandidates.forEach((item, index) => {
|
||||
policyQuestionApprovals[item.id] = {
|
||||
status: 'APPROVED',
|
||||
answerDisposition: item.category === 'NO_EVIDENCE'
|
||||
? 'FORMAL_NO_EVIDENCE_BOUNDARY'
|
||||
: item.category === 'UNAUTHORIZED'
|
||||
? 'FORMAL_UNAUTHORIZED_BOUNDARY'
|
||||
: 'FORMALLY_SOURCED',
|
||||
sourceRefs: ['FORMAL-OPS-001:section-2'],
|
||||
expectedBehavior: item.expectedBehavior,
|
||||
observedBehavior: 'test-only observed result',
|
||||
evidenceRef: `evidence/question-${index + 1}.json`,
|
||||
evidenceSha256: 'b'.repeat(64),
|
||||
reviewedBy: `question-reviewer-${index + 1}`,
|
||||
reviewedAt: '2026-07-30T11:00:00+08:00',
|
||||
}
|
||||
})
|
||||
const signoffs = Object.fromEntries([
|
||||
'businessOwner',
|
||||
'knowledgeOwner',
|
||||
'trainingOwner',
|
||||
'channelOwner',
|
||||
'releaseOwner',
|
||||
].map((role) => [role, {
|
||||
status: 'APPROVED',
|
||||
reviewedBy: `${role}-reviewer`,
|
||||
reviewedAt: '2026-07-30T12:00:00+08:00',
|
||||
}]))
|
||||
fs.writeFileSync(approvalPath, JSON.stringify({
|
||||
schemaVersion: '1.0',
|
||||
releaseTarget: '2026-08-01',
|
||||
tenantId: '000000',
|
||||
candidateFile: 'aug1-life-advisor-content-candidates-v0.1.json',
|
||||
candidateSha256: crypto.createHash('sha256').update(bytes).digest('hex'),
|
||||
releaseStatus: 'APPROVED',
|
||||
formalContentVersion: 'aug1-formal-v1',
|
||||
sourceRegistry: [source],
|
||||
scenarioApprovals,
|
||||
policyQuestionApprovals,
|
||||
signoffs,
|
||||
}))
|
||||
fs.writeFileSync(rowsPath, rows.join('\n') + '\n')
|
||||
NODE
|
||||
|
||||
mock_bin="$test_tmp/mock-bin"
|
||||
mkdir -p "$mock_bin"
|
||||
cat > "$mock_bin/systemctl" <<'MOCK_SYSTEMCTL'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
printf '%s\n' "$MOCK_SERVICE_PID"
|
||||
MOCK_SYSTEMCTL
|
||||
cat > "$mock_bin/mysql" <<'MOCK_MYSQL'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
query="$(cat)"
|
||||
if [[ "$query" == *'COUNT(DISTINCT u.user_id)'* ]]; then
|
||||
handler_count="${MOCK_HANDLER_COUNT:-2}"
|
||||
printf 'direct_audit\t%s\n' "$handler_count"
|
||||
printf 'direct_finance\t%s\n' "$handler_count"
|
||||
printf 'direct_hr\t%s\n' "$handler_count"
|
||||
printf 'direct_operations\t%s\n' "$handler_count"
|
||||
printf 'direct_president\t%s\n' "$handler_count"
|
||||
elif [[ "$query" == *'FROM aihr_practice_scenario'* ]]; then
|
||||
cat "$MOCK_SCENARIO_ROWS"
|
||||
else
|
||||
echo 'FAIL: unexpected mock MySQL query' >&2
|
||||
exit 90
|
||||
fi
|
||||
MOCK_MYSQL
|
||||
cat > "$mock_bin/ssh" <<'MOCK_SSH'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
while (($#)); do
|
||||
if [[ "$1" == '--' ]]; then
|
||||
shift
|
||||
break
|
||||
fi
|
||||
shift
|
||||
done
|
||||
remote_script="$MOCK_REMOTE_SCRIPT"
|
||||
cat > "$remote_script"
|
||||
export MOCK_SERVICE_PID="$$"
|
||||
PATH="$MOCK_REMOTE_BIN:$PATH" bash "$remote_script" "$@"
|
||||
MOCK_SSH
|
||||
chmod +x "$mock_bin/systemctl" "$mock_bin/mysql" "$mock_bin/ssh"
|
||||
|
||||
run_mock_readiness() {
|
||||
PATH="$mock_bin:$PATH" \
|
||||
MOCK_REMOTE_BIN="$mock_bin" \
|
||||
MOCK_REMOTE_SCRIPT="$test_tmp/remote.sh" \
|
||||
MOCK_SCENARIO_ROWS="$test_tmp/scenario-rows.tsv" \
|
||||
AIHR_SMS_DEV_FIXED_CODE='test-only-fixed-code' \
|
||||
AIHR_SMS_PROD_FIXED_CODE_ENABLED="${AIHR_SMS_PROD_FIXED_CODE_ENABLED:-true}" \
|
||||
AIHR_AUG1_APPROVAL_PATH="$test_tmp/approval.json" \
|
||||
"$SCRIPT" --execute
|
||||
}
|
||||
|
||||
ready_output="$(run_mock_readiness)"
|
||||
grep -Fq 'AUG1_FIXED_CODE_NO_REAL_SMS_MODE=PASS' <<<"$ready_output"
|
||||
grep -Fq 'AUG1_FORMAL_SCENARIOS=5/5' <<<"$ready_output"
|
||||
grep -Fq 'AUG1_RELEASE_READINESS=PASS' <<<"$ready_output"
|
||||
|
||||
handler_failure="$(
|
||||
MOCK_HANDLER_COUNT=1 run_mock_readiness 2>&1 || true
|
||||
)"
|
||||
grep -Fq 'requires an active primary handler and backup; got 1/2' <<<"$handler_failure"
|
||||
|
||||
fixed_mode_failure="$(
|
||||
AIHR_SMS_PROD_FIXED_CODE_ENABLED=false run_mock_readiness 2>&1 || true
|
||||
)"
|
||||
grep -Fq 'production fixed-code mode is not explicitly enabled' <<<"$fixed_mode_failure"
|
||||
|
||||
grep -Fq 'AIHR_SMS_DEV_FIXED_CODE' "$SCRIPT"
|
||||
grep -Fq 'AIHR_SMS_PROD_FIXED_CODE_ENABLED' "$SCRIPT"
|
||||
grep -Fq 'AUG1_FIXED_CODE_NO_REAL_SMS_MODE=PASS' "$SCRIPT"
|
||||
grep -Fq 'COUNT(DISTINCT u.user_id)' "$SCRIPT"
|
||||
grep -Fq 'handler_count >= 2' "$SCRIPT"
|
||||
grep -Fq 'u.status = '\''0'\''' "$SCRIPT"
|
||||
grep -Fq 'u.del_flag = '\''0'\''' "$SCRIPT"
|
||||
grep -Fq 'review_status" == "已发布"' "$SCRIPT"
|
||||
grep -Fq 'second_reviewer_user_id" != "$reviewer_user_id"' "$SCRIPT"
|
||||
grep -Fq 'production content hash does not match the approved snapshot' "$SCRIPT"
|
||||
grep -Fq 'AUG1_FORMAL_SCENARIOS=5/5' "$SCRIPT"
|
||||
grep -Fq 'AUG1_FORMAL_POLICY_QUESTIONS=30/30' "$SCRIPT"
|
||||
|
||||
if grep -Eiq '(^|[[:space:]])(insert|update|delete|replace|alter|drop|truncate)[[:space:]]' "$SCRIPT"; then
|
||||
echo 'FAIL: August 1 release readiness verifier contains a mutating SQL verb' >&2
|
||||
exit 1
|
||||
fi
|
||||
if grep -Fq '/resource/sms/code' "$SCRIPT" || grep -Fq '/auth/mobile/sms-login' "$SCRIPT"; then
|
||||
echo 'FAIL: final readiness verifier must inspect fixed-code state without requesting or consuming a code' >&2
|
||||
exit 1
|
||||
fi
|
||||
if grep -Eq 'echo[[:space:]].*(fixed_code|reviewedBy|user_id)' "$SCRIPT"; then
|
||||
echo 'FAIL: final readiness verifier may expose a code or identity' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
grep -Fq 'verify_aug1_readiness="${RELEASE_VERIFY_AUG1_READINESS:-false}"' "$PREFLIGHT"
|
||||
grep -Fq 'verify-aug1-release-readiness.sh" --execute' "$PREFLIGHT"
|
||||
grep -Fq 'RELEASE_VERIFY_AUG1_READINESS=true' "$DEV_SETUP"
|
||||
|
||||
echo 'PASS: August 1 final readiness gate is fixed-code-only, read-only, content-bound and handler-bound'
|
||||
@@ -12,22 +12,27 @@ $requiredFragments = @(
|
||||
'Frozen APK hash mismatch',
|
||||
'Frozen backend JAR hash mismatch',
|
||||
'Content candidate hash mismatch',
|
||||
'Release approval candidate hash mismatch',
|
||||
"'operations/release-backend.sh'",
|
||||
"'operations/release-preflight.sh'",
|
||||
"'operations/verify-aug1-production-api-readonly.sh'",
|
||||
"'operations/verify-aug1-authenticated-production.sh'",
|
||||
"'operations/verify-aug1-content-candidates.mjs'",
|
||||
"'operations/verify-aug1-formal-content.mjs'",
|
||||
"'operations/verify-aug1-release-readiness.sh'",
|
||||
"'operations/capture-android-acceptance.ps1'",
|
||||
"'operations/go-no-go.md'",
|
||||
"'operations/business-content-and-five-channel-signoff.md'",
|
||||
'Release package already exists and will not be overwritten',
|
||||
'Release package expected 17 files',
|
||||
'ZIP verification expected 17 entries',
|
||||
'Release package expected 20 files',
|
||||
'ZIP verification expected 20 entries',
|
||||
'ZIP entry hash mismatch',
|
||||
'package_verified=true',
|
||||
'authenticatedFixedCodeSmokePassed = $true',
|
||||
'fixedCodeNoRealSmsConfirmed = $true',
|
||||
'deployPlanSmsOrLoginTriggered = $false'
|
||||
'deployPlanSmsOrLoginTriggered = $false',
|
||||
"artifact = 'aug1-release-approval.json'",
|
||||
'approvedPolicyQuestions = $approvedQuestionCount'
|
||||
)
|
||||
foreach ($fragment in $requiredFragments) {
|
||||
if (-not $source.Contains($fragment)) {
|
||||
|
||||
@@ -17,6 +17,11 @@ schema_without_url="$(
|
||||
)"
|
||||
grep -Fq 'remote verification flags require RELEASE_REMOTE_URL' <<<"$schema_without_url"
|
||||
|
||||
aug1_readiness_without_url="$(
|
||||
RELEASE_VERIFY_AUG1_READINESS=true bash "$SCRIPT" 2>&1 || true
|
||||
)"
|
||||
grep -Fq 'remote verification flags require RELEASE_REMOTE_URL' <<<"$aug1_readiness_without_url"
|
||||
|
||||
if grep -Fq 'RELEASE_VERIFY_REMOTE_BACKEND=true requires RELEASE_VERIFY_REMOTE_MATCH=true' "$SCRIPT"; then
|
||||
echo 'FAIL: backend-only verification is still coupled to static resource matching' >&2
|
||||
exit 1
|
||||
@@ -32,10 +37,13 @@ static_scope_line="$(grep -n '^[[:space:]]*if \[\[ "\$verify_remote_match" == "t
|
||||
|
||||
grep -Fq 'require_static_artifacts="$verify_remote_match"' "$SCRIPT"
|
||||
grep -Fq 'require_backend_artifact="$verify_remote_backend"' "$SCRIPT"
|
||||
grep -Fq 'verify_aug1_readiness="${RELEASE_VERIFY_AUG1_READINESS:-false}"' "$SCRIPT"
|
||||
grep -Fq 'verify-aug1-release-readiness.sh" --execute' "$SCRIPT"
|
||||
grep -Fq 'backend_jar="${RELEASE_LOCAL_BACKEND_PATH:-backend/ruoyi-admin/target/ruoyi-admin.jar}"' "$SCRIPT"
|
||||
grep -Fq 'artifact_commit="${RELEASE_ARTIFACT_COMMIT:-HEAD}"' "$SCRIPT"
|
||||
grep -Fq 'git merge-base --is-ancestor "$artifact_commit_sha" HEAD' "$SCRIPT"
|
||||
grep -Fq 'RELEASE_VERIFY_REMOTE_BACKEND=true RELEASE_VERIFY_REMOTE_SCHEMA=true ./scripts/release-preflight.sh' "$DEV_SETUP"
|
||||
grep -Fq 'RELEASE_VERIFY_REMOTE_MATCH=true RELEASE_VERIFY_REMOTE_BACKEND=true RELEASE_VERIFY_REMOTE_SCHEMA=true ./scripts/release-preflight.sh' "$DEV_SETUP"
|
||||
grep -Fq 'RELEASE_VERIFY_AUG1_READINESS=true ./scripts/release-preflight.sh' "$DEV_SETUP"
|
||||
grep -Fq 'RELEASE_VERIFY_REMOTE_MATCH=true RELEASE_VERIFY_REMOTE_BACKEND=true RELEASE_VERIFY_REMOTE_SCHEMA=true RELEASE_VERIFY_AUG1_READINESS=true ./scripts/release-preflight.sh' "$DEV_SETUP"
|
||||
|
||||
echo 'PASS: release preflight supports scoped backend/schema verification and preserves the full-package mode'
|
||||
echo 'PASS: release preflight supports scoped artifact checks and a strict August 1 business-readiness mode'
|
||||
|
||||
Reference in New Issue
Block a user