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
9 changes: 5 additions & 4 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
name: github-delivery
description: >
GitHub issue/PR lifecycle skill: PRDs, triage/QA, research, linked/open-work
PRs, external tracker delivery, competing PR analysis, stacks, review/fix/
simplify/security, conflicts, watch/status, supersede/overtake, merge/closure.
Watch MUST run scripts/ship-gate.mjs every wake. Default mode is read-only.
Not for local pre-PR debugging, non-GitHub planning, or skill authoring.
PRs, external tracker delivery, competing PR analysis, backports/ports, stacks,
review/fix/simplify/security, conflicts, watch/status, supersede/overtake,
merge/closure. Watch MUST run scripts/ship-gate.mjs every wake. Default mode is
read-only. Not for local pre-PR debugging, non-GitHub planning, or skill authoring.
---

# GitHub Delivery
Expand Down Expand Up @@ -46,6 +46,7 @@ simplify preparation first, then enter the merge workflow.
| List my open PRs / what’s in review / repository open-work standup (read-only overview) | `references/open-work-status.md` |
| Inspect or deliver external work item ENG-42 through the GitHub lifecycle | `references/work-item-delivery.md` |
| Find competing / overlapping PR implementations (analysis only) | `references/consolidate-prs.md` |
| Backport / port PR #N to one or more target base branches | `references/multi-base-delivery.md` |
| Full review on PR #N (or a list); babysit to green + verdict | `references/full-review-pr.md` |
| Spec and Standards review on PR #N | `references/spec-standards-review.md` |
| Simplify / clean up / deduplicate PR #N without behavior changes | `references/simplify-pr.md` |
Expand Down
85 changes: 85 additions & 0 deletions references/multi-base-delivery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<!-- policy-modules:start -->
Policy modules:
- policy-kernel
- mutation
- evidence
- git
- ci
- reviews
- publication
- releases
- stacks (when actual stack topology is detected)
<!-- policy-modules:end -->

# Multi-base delivery

**Trigger:** “backport PR #42 to release/1.x”, “port PR #42 to release/1.x and release/2.x”, or equivalent requests to carry one verified source change to one or more other base branches.

## Goal

Create and track one independent port per requested target base without confusing parallel ports with stacked PR topology.

A port is derived from one exact source PR head. Each target gets its own branch and pull request against that target base. A successful port to one base does not imply success on another base.

## Authority boundary

Porting authority permits only the requested implementation/publication work. It does not automatically grant merge authority for the source PR or any port PR.

If the user explicitly requests the port PRs to be merged, each merge still uses the normal `references/merge-pr.md` current-head gates. A release/backport label, source-PR merge, tracker state, or provenance marker never substitutes for merge authority.

## Planning

1. Resolve the canonical repository and source PR.
2. Re-read the source PR and pin its exact current `headRefOid` before planning. Do not plan from a remembered SHA.
3. Resolve the requested target bases exactly. Reject the source base as a target.
4. Run `planMultiBaseDelivery(...)` to create one independent `parallel-port` entry for each target.
5. Treat all requested targets as required unless the user or repository policy explicitly marks some as optional.
6. Preserve the generated provenance marker in each port PR body. The marker binds repository, source PR, source head SHA, and target base.

## Applying a port

For each target independently:

1. Create/reset only the dedicated generated port branch for that target.
2. Apply the source change onto the target base using the repository-appropriate port/backport mechanism. Resolve conflicts semantically; do not blindly choose source or target versions.
3. Run target-base-appropriate local verification.
4. Publish the branch through the normal `push_code` broker path.
5. Reuse the P0 exact duplicate/idempotency preflight before `create_pr`.
6. Create the PR against the exact target base and include the provenance marker in the body.
7. Run the normal review/status workflow for that port PR.

Ports are independent. Do not make the `release/2.x` port PR target the `release/1.x` port branch merely because both came from the same source. That would create a stack and change merge semantics.

## Verification and completion

Use `summarizeMultiBaseDelivery(...)` against live port PR evidence.

A port is recognized only when:

- its target base matches exactly; and
- its body contains the exact provenance marker for the pinned source head and target base.

Multiple matching PRs for one target are an ambiguity/error, not a reason to pick the newest one.

Overall delivery is `complete` only when every required target is verified merged. Otherwise report the exact required targets that remain `missing` or `open`.

If the source PR merges but required ports remain incomplete, do not mark the associated work item or delivery request fully done.

## Failure rules

Fail closed when:

- source repository/PR/head identity is incomplete;
- the source head changes before the port plan is applied;
- a target base is invalid or missing;
- duplicate port PRs exist for one provenance identity;
- a port PR targets the wrong base;
- provenance is missing/stale;
- target verification fails;
- required port state cannot be read authoritatively.

Report partial success per target. Do not hide a failed target behind successful sibling ports.

## Provenance

The multi-target delivery idea was informed by `OutThisLife/brooklyn-skills` delivery patterns (MIT, copyright Brooklyn Nicholson). GitHub Delivery implements ports as head-bound, independently gated PRs and deliberately keeps them distinct from stacked-PR topology.
15 changes: 14 additions & 1 deletion scripts/lib/delivery-workflow-profiles.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { basename, join, resolve } from "node:path";
import { join, resolve } from "node:path";

import {
parsePolicyDependencies,
Expand Down Expand Up @@ -113,6 +113,18 @@ const CONSOLIDATE_GRAPH = Object.freeze({
...TERMINAL,
});

const MULTI_BASE_GRAPH = Object.freeze({
ROUTE: ["PREFLIGHT"],
PREFLIGHT: ["PLAN", "DONE"],
PLAN: ["APPLY"],
APPLY: ["LOCAL_VERIFY"],
LOCAL_VERIFY: ["PUBLISH", "DONE"],
PUBLISH: ["VERIFY_PORTS"],
VERIFY_PORTS: ["FINAL_GATE", "DONE"],
FINAL_GATE: ["DONE"],
...TERMINAL,
});

const MERGE_GRAPH = Object.freeze({
ROUTE: ["PREFLIGHT"],
PREFLIGHT: ["PREPARE", "DONE"],
Expand Down Expand Up @@ -180,6 +192,7 @@ const PROFILE_DEFINITIONS = Object.freeze({
"open-work-status": { graph: OPEN_WORK_GRAPH, mutation: "read-only" },
"work-item-delivery": { graph: WORK_ITEM_GRAPH, mutation: "profile-dependent" },
"consolidate-prs": { graph: CONSOLIDATE_GRAPH, mutation: "read-only" },
"multi-base-delivery": { graph: MULTI_BASE_GRAPH, mutation: "maintainer" },
"full-review-pr": { graph: REVIEW_GRAPH, mutation: "review" },
"spec-standards-review": { graph: REVIEW_GRAPH, mutation: "review" },
"simplify-pr": { graph: REVIEW_GRAPH, mutation: "maintainer" },
Expand Down
174 changes: 174 additions & 0 deletions scripts/lib/multi-base-delivery.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { createHash } from "node:crypto";

function requiredText(value, name) {
const text = String(value ?? "").trim();
if (!text) throw new Error(`${name}_required`);
return text;
}

function positiveInteger(value, name) {
const number = Number(value);
if (!Number.isInteger(number) || number <= 0) throw new Error(`${name}_invalid`);
return number;
}

function repoName(value) {
const text = requiredText(value, "repo");
if (!/^[^/\s]+\/[^/\s]+$/.test(text)) throw new Error("repo_invalid");
return text;
}

function exactSha(value, name = "head") {
const text = requiredText(value, name).toLowerCase();
if (!/^[0-9a-f]{40,64}$/.test(text)) throw new Error(`${name}_invalid`);
return text;
}

function validRef(value, name) {
const ref = requiredText(value, name);
const components = ref.split("/");
const invalidCharacter = ["~", "^", ":", "?", "*", "[", "\\"].some((character) => ref.includes(character));
const invalidComponent = components.some((component) =>
!component || component.startsWith(".") || component.endsWith(".lock"),
);
if (
/[\x00-\x20\x7f]/.test(ref) || invalidCharacter || invalidComponent ||
ref === "@" || ref.startsWith("-") || ref.includes("..") || ref.includes("@{") || ref.endsWith(".")
) {
throw new Error(`${name}_invalid`);
}
return ref;
}

function slug(value) {
return String(value).replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "target";
}

function shortHash(value) {
return createHash("sha256").update(String(value), "utf8").digest("hex").slice(0, 10);
}

function portBranch(sourcePr, targetBase) {
return `github-delivery/port-${sourcePr}-to-${slug(targetBase)}-${shortHash(targetBase)}`;
}

function markerPayload({ repository, sourcePr, sourceHeadSha, targetBase }) {
return `${repository}\u0000${sourcePr}\u0000${sourceHeadSha}\u0000${targetBase}`;
}

export function portProvenanceMarker(input = {}) {
const repository = repoName(input.repository);
const sourcePr = positiveInteger(input.sourcePr, "source_pr");
const sourceHeadSha = exactSha(input.sourceHeadSha, "source_head");
const targetBase = validRef(input.targetBase, "target_base");
const digest = createHash("sha256").update(markerPayload({ repository, sourcePr, sourceHeadSha, targetBase })).digest("hex");
return `<!-- github-delivery:port ${digest} -->`;
}

export function planMultiBaseDelivery({ repository, sourcePr, sourceHeadSha, sourceBase, targetBases = [], requiredBases = null } = {}) {
const repo = repoName(repository);
const pr = positiveInteger(sourcePr, "source_pr");
const head = exactSha(sourceHeadSha, "source_head");
const base = validRef(sourceBase, "source_base");
const targets = [...new Set((targetBases || []).map((value) => validRef(value, "target_base")))];
if (targets.length === 0) throw new Error("target_bases_required");
if (targets.includes(base)) throw new Error(`target_base_matches_source:${base}`);

const requiredSet = requiredBases === null
? new Set(targets)
: new Set((requiredBases || []).map((value) => validRef(value, "required_base")));
for (const required of requiredSet) {
if (!targets.includes(required)) throw new Error(`required_base_not_targeted:${required}`);
}

const ports = targets.map((targetBase) => ({
repository: repo,
sourcePr: pr,
sourceHeadSha: head,
sourceBase: base,
targetBase,
required: requiredSet.has(targetBase),
branch: portBranch(pr, targetBase),
provenanceMarker: portProvenanceMarker({ repository: repo, sourcePr: pr, sourceHeadSha: head, targetBase }),
topology: "parallel-port",
}));

return {
schemaVersion: 1,
kind: "github-delivery/multi-base-plan",
repository: repo,
sourcePr: pr,
sourceHeadSha: head,
sourceBase: base,
ports,
};
}

export function verifyPortPullRequest(port, observed = {}) {
const number = positiveInteger(observed.number, "port_pr");
const targetBase = requiredText(observed.base, "port_base");
const body = String(observed.body ?? "");
if (targetBase !== port.targetBase) {
return { state: "mismatch", number, targetBase, reason: `port_base_mismatch:${targetBase}` };
}
if (!body.includes(port.provenanceMarker)) {
return { state: "mismatch", number, targetBase, reason: "port_provenance_missing" };
}
return {
state: "verified",
number,
targetBase,
merged: observed.merged === true,
url: observed.url ? String(observed.url) : null,
};
}

export function summarizeMultiBaseDelivery({ plan, observedPullRequests = [] } = {}) {
if (plan?.kind !== "github-delivery/multi-base-plan") throw new Error("multi_base_plan_required");
const observations = new Map();
const invalid = [];

for (const observed of observedPullRequests || []) {
const body = String(observed?.body ?? "");
const matching = plan.ports.filter((port) => body.includes(port.provenanceMarker));
if (matching.length === 0) continue;
if (matching.length > 1) {
invalid.push({
number: Number(observed?.number) || null,
reason: "port_provenance_ambiguous",
targetBases: matching.map((port) => port.targetBase),
});
continue;
}

const port = matching[0];
const verification = verifyPortPullRequest(port, observed);
if (verification.state !== "verified") {
invalid.push({
number: verification.number,
reason: verification.reason,
expectedTargetBase: port.targetBase,
observedTargetBase: verification.targetBase,
});
continue;
}
if (observations.has(port.targetBase)) throw new Error(`duplicate_port_pr:${port.targetBase}`);
observations.set(port.targetBase, verification);
}

const ports = plan.ports.map((port) => {
const observed = observations.get(port.targetBase) || null;
return {
...port,
observed,
state: !observed ? "missing" : observed.merged ? "merged" : "open",
};
});
const requiredIncomplete = ports.filter((port) => port.required && port.state !== "merged");
return {
state: invalid.length > 0 ? "invalid" : requiredIncomplete.length === 0 ? "complete" : "incomplete",
ports,
invalid,
requiredIncomplete: requiredIncomplete.map((port) => port.targetBase),
};
}
12 changes: 12 additions & 0 deletions scripts/lib/skill-router.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const WORK_ITEM_STATUS_REQUEST = /\b(?:what(?:'s| is) left|status|where is|where
const WORK_ITEM_DELIVERY_REQUEST = /\b(?:ship|deliver|work on|implement|fix|finish|complete|take)\b|\b(?:create|open)\b[\s\S]{0,80}\b(?:pr|pull request)\b/;
const WORK_ITEM_PUBLICATION_REQUEST = /\b(?:ship|deliver)\b|\b(?:create|open)\b[\s\S]{0,80}\b(?:pr|pull request)\b/;
const CONSOLIDATE_PR_REQUEST = /\b(?:consolidate|cluster|triage|competing|overlapping|duplicate)\b[\s\S]{0,120}\b(?:prs|pull requests)\b|\b(?:prs|pull requests)\b[\s\S]{0,120}\b(?:competing|overlapping|duplicates?)\b/;
const MULTI_BASE_REQUEST = /\b(?:backport|back-port|port)\b[\s\S]{0,180}\b(?:pr|pull request)\s*#?\d+\b|\b(?:pr|pull request)\s*#?\d+\b[\s\S]{0,180}\b(?:backport|back-port|port)\b/;
const DELIVERY_NAME = /\bgithub[- ]?delivery\b/;
const DELIVERY_UPDATE = /\b(update|upgrade)\b[\s\S]*\bgithub[- ]?delivery\b|\bgithub[- ]?delivery\b[\s\S]*\b(update|upgrade|latest stable release)\b/;
const DELIVERY_CONFIG = /\b(set ?up|install|configure|configuration|settings?|protection mode|windows hello)\b[\s\S]*\bgithub[- ]?delivery\b|\bgithub[- ]?delivery\b[\s\S]*\b(set ?up|install|configure|configuration|settings?|protection mode|windows hello)\b/;
Expand All @@ -49,6 +50,14 @@ function workItemDeliveryActions(text) {
return [...new Set(actions)];
}

function multiBaseDeliveryActions(text) {
const actions = ["push_code", "create_pr"];
if (hasExplicitMergeIntent(text)) {
actions.push("merge_pr", "post_comment", "post_issue_comment", "close_linked_issue");
}
return actions;
}

function unquotedText(text) { return text.replace(/"[^"\n]*"|`[^`\n]*`|'[^'\n]*'/g, " "); }
function mergeText(text) { return unquotedText(text).replace(MERGE_READY_PHRASE, ""); }

Expand Down Expand Up @@ -104,6 +113,9 @@ export function routeShippingGithubPrompt(prompt) {
if (CONSOLIDATE_PR_REQUEST.test(text) && !RESEARCH_ISSUE_REQUEST.test(text)) {
return result("references/consolidate-prs.md", "read-only", []);
}
if (MULTI_BASE_REQUEST.test(text)) {
return result("references/multi-base-delivery.md", "maintainer", multiBaseDeliveryActions(text));
}
if (isWorkItemRequest(text)) {
const readOnly = WORK_ITEM_STATUS_REQUEST.test(text) && !WORK_ITEM_DELIVERY_REQUEST.test(text);
return result(
Expand Down
1 change: 1 addition & 0 deletions scripts/lib/workflow-mode.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const WORKFLOW_MUTATION_MODES = Object.freeze({
"references/fix-pr-bots.md": ["maintainer"],
"references/full-review-pr.md": ["review", "maintainer"],
"references/merge-pr.md": ["maintainer"],
"references/multi-base-delivery.md": ["maintainer"],
"references/open-work-status.md": ["read-only"],
"references/overtake-pr.md": ["maintainer"],
"references/re-review-pr.md": ["review"],
Expand Down
Loading