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
46 changes: 42 additions & 4 deletions .github/actions/path-classifier/classify.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
const fs = require('fs');
const path = require('path');
const { execFileSync } = require('child_process');
const vm = require('node:vm');

const OUTPUT_NAMES = {
'docs-only': 'is-docs-only',
Expand Down Expand Up @@ -222,9 +223,42 @@ function stableDeliverySealStatus(githubContext, { contract, now } = {}) {
};
}

function loadDeliveryContract() {
function compileDeliveryContract(source, filename) {
const module = { exports: {} };
const sandbox = { module, exports: module.exports };
vm.runInNewContext(String(source), sandbox, { filename });
return module.exports;
}

function readContractAtRef(ref, contractPath) {
return runGit(['show', `${ref}:${contractPath}`]);
}

function loadDeliveryContract(
githubContext = {},
{ readTrustedContract = readContractAtRef } = {},
) {
const workspace = process.env.GITHUB_WORKSPACE || process.cwd();
const contractPath = path.resolve(workspace, '.github/scripts/sync_pr_lease_contract.js');
const relativeContractPath = '.github/scripts/sync_pr_lease_contract.js';
const pullRequest = githubContext?.event?.pull_request;
const branch = pullRequest?.head?.ref || '';

if (githubContext?.event_name === 'pull_request' && STABLE_SYNC_BRANCHES.has(branch)) {
const baseSha = pullRequest?.base?.sha || '';
if (!baseSha) {
return null;
}
try {
const source = readTrustedContract(baseSha, relativeContractPath);
return compileDeliveryContract(source, `${baseSha}:${relativeContractPath}`);
} catch {
// Stable generated deliveries fail closed when the trusted base contract
// cannot be loaded; never fall back to the candidate checkout.
return null;
}
}

const contractPath = path.resolve(workspace, relativeContractPath);
if (!fs.existsSync(contractPath)) {
return null;
}
Expand Down Expand Up @@ -361,8 +395,12 @@ function writeOutputs(outputs) {

function main() {
const githubContext = parseGithubContext();
const baseRef = resolveBaseRef(process.env.INPUT_BASE_REF || '', githubContext);
// The stable-delivery contract is loaded from the exact trusted base SHA.
// Make that object available before evaluating the seal.
fetchBaseRef(baseRef, githubContext);
const seal = stableDeliverySealStatus(githubContext, {
contract: loadDeliveryContract(),
contract: loadDeliveryContract(githubContext),
});
if (seal.required && !seal.valid) {
throw new Error(
Expand All @@ -372,7 +410,6 @@ function main() {
}
const forceFull = String(process.env.INPUT_FORCE_FULL || '').toLowerCase() === 'true';
const configPath = process.env.INPUT_CONFIG_PATH || '.github/path-classification.yml';
const baseRef = resolveBaseRef(process.env.INPUT_BASE_REF || '', githubContext);
const config = loadConfig(configPath);
let files = [];
let conservativeFull = false;
Expand Down Expand Up @@ -400,6 +437,7 @@ module.exports = {
classifyFiles,
globToRegExp,
loadConfig,
loadDeliveryContract,
matchesAny,
normalizePath,
parseClassificationConfig,
Expand Down
35 changes: 34 additions & 1 deletion .github/scripts/__tests__/path-classifier.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const {
DEFAULT_CATEGORIES,
classifyFiles,
globToRegExp,
loadDeliveryContract,
matchesAny,
parseClassificationConfig,
stableDeliverySealStatus,
Expand Down Expand Up @@ -129,7 +130,10 @@ function deliveryContext(record, { branch = 'sync/workflows-delivery', fork = fa
sha: 'head-abc',
repo: { full_name: fork ? 'attacker/Ready' : 'stranske/Ready' },
},
base: { repo: { full_name: 'stranske/Ready' } },
base: {
sha: 'trusted-base-sha',
repo: { full_name: 'stranske/Ready' },
},
},
},
};
Expand Down Expand Up @@ -159,6 +163,35 @@ test('custom Gate classifier rejects an unsealed stable delivery', () => {
);
});

test('stable delivery loads its seal contract from the exact trusted base SHA', () => {
let requested = null;
const contractSource = require('node:fs').readFileSync(
require('node:path').join(__dirname, '..', 'sync_pr_lease_contract.js'),
'utf8',
);
const contract = loadDeliveryContract(deliveryContext(deliveryRecord), {
readTrustedContract: (ref, contractPath) => {
requested = { ref, contractPath };
return contractSource;
},
});

assert.deepEqual(requested, {
ref: 'trusted-base-sha',
contractPath: '.github/scripts/sync_pr_lease_contract.js',
});
assert.equal(contract.mergeEligibility(deliveryRecord, { requireSealed: true }).eligible, false);
});

test('stable delivery fails closed when the trusted base contract is unavailable', () => {
const contract = loadDeliveryContract(deliveryContext(deliveryRecord), {
readTrustedContract: () => {
throw new Error('base object unavailable');
},
});
assert.equal(contract, null);
});

test('custom Gate classifier accepts only the sealed exact head', () => {
const sealed = {
...deliveryRecord,
Expand Down
12 changes: 11 additions & 1 deletion .github/scripts/issue_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,11 @@ def _task_has_concrete_target(item: str) -> bool:
)
_LINE_SUFFIX = re.compile(r":\d+(?:-\d+)?$")
_NODE_SUFFIX = re.compile(r"::.+$") # pytest node ids: tests/x.py::test_y
_ORIGINAL_ISSUE_BLOCK_RE = re.compile(
r"<details\b[^>]*>\s*<summary>Original Issue</summary>\s*"
r"(?P<fence>`{3,})text\s*\n(?P<inner>.*?)\n(?P=fence)\s*</details>",
re.DOTALL | re.IGNORECASE,
)
# Self-referential boilerplate. The format contract tells authors to cite it, so
# nearly every body mentions it — and it lives in every repo, which means
# counting it as evidence would let one boilerplate line defeat the whole gate.
Expand Down Expand Up @@ -463,6 +468,11 @@ def _without_fenced_code(text: str) -> str:
return "\n".join(kept)


def _strip_original_issue_blocks(text: str) -> str:
"""Remove only the formatter's canonical fenced provenance block."""
return _ORIGINAL_ISSUE_BLOCK_RE.sub("", text).rstrip()


@dataclass
class Report:
ok: bool = True
Expand Down Expand Up @@ -509,7 +519,7 @@ def validate(body: str, repo_root: Path | None = None) -> Report:
the validator stays a pure body check for callers that have no checkout.
"""
report = Report()
body = body or ""
body = _strip_original_issue_blocks(body or "")
for name, aliases in REQUIRED.items():
if _find(body, aliases) is None:
report.missing_required.append(name)
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/pr-00-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ jobs:
contents: read
pull-requests: read
steps:
- name: Checkout delivery contract
- name: Checkout trusted base delivery contract
if: >-
${{
github.event_name == 'pull_request' &&
Expand All @@ -173,8 +173,8 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
repository: ${{ github.event.pull_request.head.repo.full_name }}
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.base.repo.full_name }}
ref: ${{ github.event.pull_request.base.sha }}
sparse-checkout: .github/scripts/sync_pr_lease_contract.js
sparse-checkout-cone-mode: false

Expand Down
13 changes: 10 additions & 3 deletions docs/ops/CONSUMER_REPO_MAINTENANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,11 @@ For these repos:
- The Gate workflow (`pr-00-gate.yml`) is maintained locally and excluded from sync.
- A custom Gate must invoke the exact-synced `.github/actions/path-classifier`
(or enforce the equivalent delivery-record check itself). The classifier
rejects stable candidate/delivery PRs until Maint 71 seals the exact head, so
a custom aggregate `Gate / gate` cannot report success while delivery is
still mutable.
loads the delivery-seal contract from the pull request's exact trusted base
SHA and fails closed when that object is unavailable; it never evaluates the
candidate copy of the contract. It rejects stable candidate/delivery PRs
until Maint 71 seals the exact head, so a custom aggregate `Gate / gate`
cannot report success while delivery is still mutable.
- `Trend_Model_Project` skips the synced `AGENTS.md` file and keeps its local
`Agents.md`.
- `trip-planner` skips the synced `.github/scripts/package.json` and vendored
Expand Down Expand Up @@ -341,6 +343,11 @@ Gate summary rejects an unsealed stable delivery, while the shared merge guard
rejects `sync:delivery-staging` for every merger except Maint 71's verified
sealed path. The staging hold remains until the merge succeeds.

The standard Gate's generated-delivery job also checks out
`sync_pr_lease_contract.js` from the exact pull-request base SHA, not from the
candidate head. A contract change therefore cannot define its own acceptance
rule. Missing or unreadable trusted-base enforcement code is a hard failure.

Generated `sync/workflows-*` PRs are excluded from both the basic and agent
autofix lanes. Their intentional pre-seal Gate failure is a delivery hold, not
a request for a consumer-branch repair commit; Maint 71 advances the record and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
const fs = require('fs');
const path = require('path');
const { execFileSync } = require('child_process');
const vm = require('node:vm');

const OUTPUT_NAMES = {
'docs-only': 'is-docs-only',
Expand Down Expand Up @@ -222,9 +223,42 @@ function stableDeliverySealStatus(githubContext, { contract, now } = {}) {
};
}

function loadDeliveryContract() {
function compileDeliveryContract(source, filename) {
const module = { exports: {} };
const sandbox = { module, exports: module.exports };
vm.runInNewContext(String(source), sandbox, { filename });
return module.exports;
}

function readContractAtRef(ref, contractPath) {
return runGit(['show', `${ref}:${contractPath}`]);
}

function loadDeliveryContract(
githubContext = {},
{ readTrustedContract = readContractAtRef } = {},
) {
const workspace = process.env.GITHUB_WORKSPACE || process.cwd();
const contractPath = path.resolve(workspace, '.github/scripts/sync_pr_lease_contract.js');
const relativeContractPath = '.github/scripts/sync_pr_lease_contract.js';
const pullRequest = githubContext?.event?.pull_request;
const branch = pullRequest?.head?.ref || '';

if (githubContext?.event_name === 'pull_request' && STABLE_SYNC_BRANCHES.has(branch)) {
const baseSha = pullRequest?.base?.sha || '';
if (!baseSha) {
return null;
}
try {
const source = readTrustedContract(baseSha, relativeContractPath);
return compileDeliveryContract(source, `${baseSha}:${relativeContractPath}`);
} catch {
// Stable generated deliveries fail closed when the trusted base contract
// cannot be loaded; never fall back to the candidate checkout.
return null;
}
}

const contractPath = path.resolve(workspace, relativeContractPath);
if (!fs.existsSync(contractPath)) {
return null;
}
Expand Down Expand Up @@ -361,8 +395,12 @@ function writeOutputs(outputs) {

function main() {
const githubContext = parseGithubContext();
const baseRef = resolveBaseRef(process.env.INPUT_BASE_REF || '', githubContext);
// The stable-delivery contract is loaded from the exact trusted base SHA.
// Make that object available before evaluating the seal.
fetchBaseRef(baseRef, githubContext);
const seal = stableDeliverySealStatus(githubContext, {
contract: loadDeliveryContract(),
contract: loadDeliveryContract(githubContext),
});
if (seal.required && !seal.valid) {
throw new Error(
Expand All @@ -372,7 +410,6 @@ function main() {
}
const forceFull = String(process.env.INPUT_FORCE_FULL || '').toLowerCase() === 'true';
const configPath = process.env.INPUT_CONFIG_PATH || '.github/path-classification.yml';
const baseRef = resolveBaseRef(process.env.INPUT_BASE_REF || '', githubContext);
const config = loadConfig(configPath);
let files = [];
let conservativeFull = false;
Expand Down Expand Up @@ -400,6 +437,7 @@ module.exports = {
classifyFiles,
globToRegExp,
loadConfig,
loadDeliveryContract,
matchesAny,
normalizePath,
parseClassificationConfig,
Expand Down
12 changes: 11 additions & 1 deletion templates/consumer-repo/.github/scripts/issue_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,11 @@ def _task_has_concrete_target(item: str) -> bool:
)
_LINE_SUFFIX = re.compile(r":\d+(?:-\d+)?$")
_NODE_SUFFIX = re.compile(r"::.+$") # pytest node ids: tests/x.py::test_y
_ORIGINAL_ISSUE_BLOCK_RE = re.compile(
r"<details\b[^>]*>\s*<summary>Original Issue</summary>\s*"
r"(?P<fence>`{3,})text\s*\n(?P<inner>.*?)\n(?P=fence)\s*</details>",
re.DOTALL | re.IGNORECASE,
)
# Self-referential boilerplate. The format contract tells authors to cite it, so
# nearly every body mentions it — and it lives in every repo, which means
# counting it as evidence would let one boilerplate line defeat the whole gate.
Expand Down Expand Up @@ -463,6 +468,11 @@ def _without_fenced_code(text: str) -> str:
return "\n".join(kept)


def _strip_original_issue_blocks(text: str) -> str:
"""Remove only the formatter's canonical fenced provenance block."""
return _ORIGINAL_ISSUE_BLOCK_RE.sub("", text).rstrip()


@dataclass
class Report:
ok: bool = True
Expand Down Expand Up @@ -509,7 +519,7 @@ def validate(body: str, repo_root: Path | None = None) -> Report:
the validator stays a pure body check for callers that have no checkout.
"""
report = Report()
body = body or ""
body = _strip_original_issue_blocks(body or "")
for name, aliases in REQUIRED.items():
if _find(body, aliases) is None:
report.missing_required.append(name)
Expand Down
6 changes: 3 additions & 3 deletions templates/consumer-repo/.github/workflows/pr-00-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ jobs:
contents: read
pull-requests: read
steps:
- name: Checkout delivery contract
- name: Checkout trusted base delivery contract
if: >-
${{
github.event_name == 'pull_request' &&
Expand All @@ -182,8 +182,8 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
repository: ${{ github.event.pull_request.head.repo.full_name }}
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.base.repo.full_name }}
ref: ${{ github.event.pull_request.base.sha }}
sparse-checkout: .github/scripts/sync_pr_lease_contract.js
sparse-checkout-cone-mode: false

Expand Down
4 changes: 4 additions & 0 deletions templates/consumer-repo/docs/AGENT_ISSUE_FORMAT.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,10 @@ Tasks
different action.
quoted and unquoted task paths both count, while absolute and parent-relative
paths never count as repository evidence.
[ ] Ignore paths preserved inside the formatter's archived
`<summary>Original Issue</summary>` provenance block. Only the visible issue
body is live work-order evidence; malformed or unclosed archives remain
visible and fail closed.
[ ] No banned vague verb stands alone ("fix bugs", "improve X",
"update things", "clean up", "refactor", "optimize", "polish").
[ ] Each task is atomic — one checkbox = one discrete, verifiable change.
Expand Down
Loading
Loading