@openthink/stamp 1.5.2 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -9
- package/dist/hooks/pre-receive.cjs.map +1 -1
- package/dist/index.js +599 -15
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -490,10 +490,20 @@ function validateConfig(input) {
|
|
|
490
490
|
}
|
|
491
491
|
require_human_merge = r.require_human_merge;
|
|
492
492
|
}
|
|
493
|
+
let strict_base;
|
|
494
|
+
if (r.strict_base !== void 0) {
|
|
495
|
+
if (typeof r.strict_base !== "boolean") {
|
|
496
|
+
throw new Error(
|
|
497
|
+
`config.branches.${name}.strict_base must be a boolean`
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
strict_base = r.strict_base;
|
|
501
|
+
}
|
|
493
502
|
branches[name] = {
|
|
494
503
|
required: r.required.map(String),
|
|
495
504
|
...required_checks ? { required_checks } : {},
|
|
496
|
-
...require_human_merge !== void 0 ? { require_human_merge } : {}
|
|
505
|
+
...require_human_merge !== void 0 ? { require_human_merge } : {},
|
|
506
|
+
...strict_base !== void 0 ? { strict_base } : {}
|
|
497
507
|
};
|
|
498
508
|
}
|
|
499
509
|
const reviewers2 = {};
|
|
@@ -2943,7 +2953,7 @@ function branchExists(name, cwd) {
|
|
|
2943
2953
|
|
|
2944
2954
|
// src/commands/init.ts
|
|
2945
2955
|
import { existsSync as existsSync9, writeFileSync as writeFileSync7 } from "fs";
|
|
2946
|
-
import { join as join5 } from "path";
|
|
2956
|
+
import { dirname as dirname4, join as join5 } from "path";
|
|
2947
2957
|
|
|
2948
2958
|
// src/lib/ghRuleset.ts
|
|
2949
2959
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
@@ -3476,6 +3486,11 @@ For local-only / advisory use against this GitHub repo: re-run with \`stamp init
|
|
|
3476
3486
|
const dbExisted = existsSync9(stateDbPath);
|
|
3477
3487
|
const db = openDb(stateDbPath);
|
|
3478
3488
|
db.close();
|
|
3489
|
+
const prCheckResult = maybeWriteVerifyWorkflow(
|
|
3490
|
+
repoRoot,
|
|
3491
|
+
opts.prCheck,
|
|
3492
|
+
effectiveMode
|
|
3493
|
+
);
|
|
3479
3494
|
const agentsMdAction = opts.agentsMd === false ? "skipped" : ensureAgentsMd(repoRoot, effectiveMode);
|
|
3480
3495
|
const claudeMdAction = opts.claudeMd === false ? "skipped" : ensureClaudeMd(repoRoot);
|
|
3481
3496
|
const scaffoldOrSync = alreadyHasConfig ? "sync" : "scaffold";
|
|
@@ -3506,6 +3521,11 @@ For local-only / advisory use against this GitHub repo: re-run with \`stamp init
|
|
|
3506
3521
|
` CLAUDE.md: ${claudeMdAction} at repo root (auto-loaded by Claude Code)`
|
|
3507
3522
|
);
|
|
3508
3523
|
}
|
|
3524
|
+
if (prCheckResult.action !== "skipped") {
|
|
3525
|
+
console.log(
|
|
3526
|
+
` PR check: ${prCheckResult.action} ${prCheckResult.path} (stamp/verify-attestation@${VERIFY_ACTION_REF})`
|
|
3527
|
+
);
|
|
3528
|
+
}
|
|
3509
3529
|
console.log(
|
|
3510
3530
|
` models: ${userCfg.path}${userCfg.created ? " (created \u2014 Sonnet defaults; tweak with `stamp config reviewers set <name> <model-id>`)" : " (existing)"}`
|
|
3511
3531
|
);
|
|
@@ -3524,6 +3544,11 @@ For local-only / advisory use against this GitHub repo: re-run with \`stamp init
|
|
|
3524
3544
|
console.error(warning);
|
|
3525
3545
|
console.error();
|
|
3526
3546
|
}
|
|
3547
|
+
if (prCheckResult.action !== "skipped") {
|
|
3548
|
+
console.log(
|
|
3549
|
+
"PR-check mode notes:\n - The workflow runs the verifier on every PR but does NOT block\n merge by itself. Wire it into branch protection so green-check\n is required before merge:\n Settings \u2192 Branches \u2192 main \u2192 Protect \u2192 Require status checks \u2192\n add `stamp verify` (the workflow's job name) as required.\n - Operator workflow per PR: stamp review \u2192 stamp attest --into main\n --push origin \u2192 open PR \u2192 check goes green \u2192 human merges.\n"
|
|
3550
|
+
);
|
|
3551
|
+
}
|
|
3527
3552
|
if (scaffoldOrSync === "scaffold") {
|
|
3528
3553
|
if (effectiveMode === "local-only") {
|
|
3529
3554
|
console.log(
|
|
@@ -3811,6 +3836,57 @@ function resolveMode(userMode, remoteClass) {
|
|
|
3811
3836
|
return { effectiveMode: "local-only", warnings };
|
|
3812
3837
|
}
|
|
3813
3838
|
}
|
|
3839
|
+
var VERIFY_ACTION_REF = "v1.6.0";
|
|
3840
|
+
function maybeWriteVerifyWorkflow(repoRoot, prCheckOpt, effectiveMode) {
|
|
3841
|
+
const path2 = ".github/workflows/stamp-verify.yml";
|
|
3842
|
+
const fullPath = join5(repoRoot, path2);
|
|
3843
|
+
const defaultForMode = effectiveMode !== "server-gated";
|
|
3844
|
+
const shouldWrite = prCheckOpt ?? defaultForMode;
|
|
3845
|
+
if (!shouldWrite) return { action: "skipped", path: path2 };
|
|
3846
|
+
if (existsSync9(fullPath)) {
|
|
3847
|
+
return { action: "exists", path: path2 };
|
|
3848
|
+
}
|
|
3849
|
+
ensureDir(dirname4(fullPath));
|
|
3850
|
+
writeFileSync7(fullPath, renderVerifyWorkflow());
|
|
3851
|
+
return { action: "wrote", path: path2 };
|
|
3852
|
+
}
|
|
3853
|
+
function renderVerifyWorkflow() {
|
|
3854
|
+
return [
|
|
3855
|
+
"name: stamp verify",
|
|
3856
|
+
"",
|
|
3857
|
+
`# Runs stamp/verify-attestation@${VERIFY_ACTION_REF} on every PR.`,
|
|
3858
|
+
"# Wire `stamp verify` (this job's name) into branch protection",
|
|
3859
|
+
"# Required Status Checks to make a green attestation a merge",
|
|
3860
|
+
"# precondition.",
|
|
3861
|
+
"",
|
|
3862
|
+
"on:",
|
|
3863
|
+
" pull_request:",
|
|
3864
|
+
" branches: [main]",
|
|
3865
|
+
"",
|
|
3866
|
+
"permissions:",
|
|
3867
|
+
" # checkout + read .stamp/{config,trusted-keys}/ from the base ref",
|
|
3868
|
+
" contents: read",
|
|
3869
|
+
" # for the workflow's check-run summary on the PR",
|
|
3870
|
+
" checks: write",
|
|
3871
|
+
"",
|
|
3872
|
+
"jobs:",
|
|
3873
|
+
" stamp-verify:",
|
|
3874
|
+
" name: stamp verify",
|
|
3875
|
+
" runs-on: ubuntu-latest",
|
|
3876
|
+
" timeout-minutes: 5",
|
|
3877
|
+
" steps:",
|
|
3878
|
+
" - name: checkout",
|
|
3879
|
+
" uses: actions/checkout@v4",
|
|
3880
|
+
" with:",
|
|
3881
|
+
" # Full history so the action can fetch the base ref's tree",
|
|
3882
|
+
" # and resolve refs/stamp/attestations/*. Shallow clones",
|
|
3883
|
+
" # would force per-step refetches.",
|
|
3884
|
+
" fetch-depth: 0",
|
|
3885
|
+
" - name: stamp/verify-attestation",
|
|
3886
|
+
` uses: OpenThinkAi/stamp-cli/.github/actions/verify-attestation@${VERIFY_ACTION_REF}`,
|
|
3887
|
+
""
|
|
3888
|
+
].join("\n");
|
|
3889
|
+
}
|
|
3814
3890
|
|
|
3815
3891
|
// src/commands/invites.ts
|
|
3816
3892
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
@@ -4346,7 +4422,7 @@ import { parse as parseYaml5 } from "yaml";
|
|
|
4346
4422
|
// src/commands/server.ts
|
|
4347
4423
|
import { spawnSync as spawnSync6 } from "child_process";
|
|
4348
4424
|
import { existsSync as existsSync11, mkdirSync as mkdirSync4, renameSync as renameSync3, unlinkSync as unlinkSync2, writeFileSync as writeFileSync8 } from "fs";
|
|
4349
|
-
import { dirname as
|
|
4425
|
+
import { dirname as dirname5 } from "path";
|
|
4350
4426
|
import { stringify as stringifyYaml2 } from "yaml";
|
|
4351
4427
|
|
|
4352
4428
|
// src/lib/perRepoKey.ts
|
|
@@ -4505,7 +4581,7 @@ function writeConfig(opts) {
|
|
|
4505
4581
|
repoRootPrefix: opts.repoRootPrefix
|
|
4506
4582
|
});
|
|
4507
4583
|
const path2 = userServerConfigPath();
|
|
4508
|
-
const dir =
|
|
4584
|
+
const dir = dirname5(path2);
|
|
4509
4585
|
if (!existsSync11(dir)) mkdirSync4(dir, { recursive: true, mode: 448 });
|
|
4510
4586
|
const tmp = `${path2}.tmp.${process.pid}`;
|
|
4511
4587
|
writeFileSync8(tmp, yaml, { mode: 384 });
|
|
@@ -5858,6 +5934,291 @@ function printReviewHistory(repoRoot, limit, diff) {
|
|
|
5858
5934
|
);
|
|
5859
5935
|
}
|
|
5860
5936
|
|
|
5937
|
+
// src/commands/attest.ts
|
|
5938
|
+
import { spawnSync as spawnSync12 } from "child_process";
|
|
5939
|
+
import { createHash as createHash7 } from "crypto";
|
|
5940
|
+
|
|
5941
|
+
// src/lib/patchId.ts
|
|
5942
|
+
import { spawnSync as spawnSync10 } from "child_process";
|
|
5943
|
+
function patchIdForSpan(base_sha, head_sha, repoRoot) {
|
|
5944
|
+
const diff = spawnSync10("git", ["diff", `${base_sha}..${head_sha}`], {
|
|
5945
|
+
cwd: repoRoot,
|
|
5946
|
+
// Buffer because diffs can be large and stable across encodings;
|
|
5947
|
+
// patch-id consumes the raw bytes.
|
|
5948
|
+
encoding: "buffer",
|
|
5949
|
+
maxBuffer: 64 * 1024 * 1024
|
|
5950
|
+
});
|
|
5951
|
+
if (diff.status !== 0) {
|
|
5952
|
+
const stderr = diff.stderr?.toString("utf8") ?? "";
|
|
5953
|
+
throw new Error(
|
|
5954
|
+
`git diff ${base_sha}..${head_sha} failed: ${stderr.trim() || "exit " + diff.status}`
|
|
5955
|
+
);
|
|
5956
|
+
}
|
|
5957
|
+
if (diff.stdout.length === 0) {
|
|
5958
|
+
throw new Error(
|
|
5959
|
+
`empty diff between ${base_sha.slice(0, 8)}..${head_sha.slice(0, 8)} \u2014 nothing to attest`
|
|
5960
|
+
);
|
|
5961
|
+
}
|
|
5962
|
+
const result = spawnSync10("git", ["patch-id", "--stable"], {
|
|
5963
|
+
cwd: repoRoot,
|
|
5964
|
+
input: diff.stdout,
|
|
5965
|
+
encoding: "utf8",
|
|
5966
|
+
maxBuffer: 1024
|
|
5967
|
+
});
|
|
5968
|
+
if (result.status !== 0) {
|
|
5969
|
+
const stderr = result.stderr ?? "";
|
|
5970
|
+
throw new Error(
|
|
5971
|
+
`git patch-id --stable failed: ${stderr.trim() || "exit " + result.status}`
|
|
5972
|
+
);
|
|
5973
|
+
}
|
|
5974
|
+
const firstLine = (result.stdout ?? "").trim().split("\n")[0] ?? "";
|
|
5975
|
+
const token = firstLine.split(/\s+/)[0];
|
|
5976
|
+
if (!token || !/^[0-9a-f]{40}$/.test(token)) {
|
|
5977
|
+
throw new Error(
|
|
5978
|
+
`unexpected git patch-id output: ${JSON.stringify(result.stdout.slice(0, 200))}`
|
|
5979
|
+
);
|
|
5980
|
+
}
|
|
5981
|
+
return token;
|
|
5982
|
+
}
|
|
5983
|
+
|
|
5984
|
+
// src/lib/prAttestation.ts
|
|
5985
|
+
import { spawnSync as spawnSync11 } from "child_process";
|
|
5986
|
+
var PR_ATTESTATION_SCHEMA_VERSION = 2;
|
|
5987
|
+
var MIN_ACCEPTED_PR_ATTESTATION_VERSION = 1;
|
|
5988
|
+
var MAX_PR_ATTESTATION_BYTES = 64 * 1024;
|
|
5989
|
+
function serializePayload2(p) {
|
|
5990
|
+
return Buffer.from(JSON.stringify(p), "utf8");
|
|
5991
|
+
}
|
|
5992
|
+
function serializeEnvelope(env) {
|
|
5993
|
+
return Buffer.from(JSON.stringify(env), "utf8");
|
|
5994
|
+
}
|
|
5995
|
+
function parseEnvelope(bytes) {
|
|
5996
|
+
if (bytes.length === 0 || bytes.length > MAX_PR_ATTESTATION_BYTES) return null;
|
|
5997
|
+
let parsed;
|
|
5998
|
+
try {
|
|
5999
|
+
parsed = JSON.parse(bytes.toString("utf8"));
|
|
6000
|
+
} catch {
|
|
6001
|
+
return null;
|
|
6002
|
+
}
|
|
6003
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
6004
|
+
const env = parsed;
|
|
6005
|
+
if (typeof env.signature !== "string" || !env.signature) return null;
|
|
6006
|
+
if (!env.payload || typeof env.payload !== "object") return null;
|
|
6007
|
+
const p = env.payload;
|
|
6008
|
+
if (typeof p.schema_version !== "number" || p.schema_version < MIN_ACCEPTED_PR_ATTESTATION_VERSION || typeof p.patch_id !== "string" || typeof p.base_sha !== "string" || typeof p.head_sha !== "string" || typeof p.target_branch !== "string" || !Array.isArray(p.approvals) || !Array.isArray(p.checks) || typeof p.signer_key_id !== "string") {
|
|
6009
|
+
return null;
|
|
6010
|
+
}
|
|
6011
|
+
if (p.schema_version >= 2 && typeof p.target_branch_tip_sha !== "string") {
|
|
6012
|
+
return null;
|
|
6013
|
+
}
|
|
6014
|
+
return env;
|
|
6015
|
+
}
|
|
6016
|
+
function attestationRefName(patch_id) {
|
|
6017
|
+
if (!/^[0-9a-f]{40}$/.test(patch_id)) {
|
|
6018
|
+
throw new Error(
|
|
6019
|
+
`patch_id must be a 40-char lowercase hex string (got ${JSON.stringify(patch_id)})`
|
|
6020
|
+
);
|
|
6021
|
+
}
|
|
6022
|
+
return `refs/stamp/attestations/${patch_id}`;
|
|
6023
|
+
}
|
|
6024
|
+
function writeAttestationRef(envelope, repoRoot) {
|
|
6025
|
+
const ref = attestationRefName(envelope.payload.patch_id);
|
|
6026
|
+
const bytes = serializeEnvelope(envelope);
|
|
6027
|
+
const hashObject = spawnSync11(
|
|
6028
|
+
"git",
|
|
6029
|
+
["hash-object", "-w", "--stdin"],
|
|
6030
|
+
{ cwd: repoRoot, input: bytes, encoding: "utf8" }
|
|
6031
|
+
);
|
|
6032
|
+
if (hashObject.status !== 0) {
|
|
6033
|
+
const stderr = hashObject.stderr ?? "";
|
|
6034
|
+
throw new Error(
|
|
6035
|
+
`git hash-object failed: ${stderr.trim() || "exit " + hashObject.status}`
|
|
6036
|
+
);
|
|
6037
|
+
}
|
|
6038
|
+
const blob_sha = (hashObject.stdout ?? "").trim();
|
|
6039
|
+
if (!/^[0-9a-f]{40}$/.test(blob_sha)) {
|
|
6040
|
+
throw new Error(`unexpected git hash-object output: ${JSON.stringify(blob_sha)}`);
|
|
6041
|
+
}
|
|
6042
|
+
const updateRef = spawnSync11(
|
|
6043
|
+
"git",
|
|
6044
|
+
["update-ref", ref, blob_sha],
|
|
6045
|
+
{ cwd: repoRoot, encoding: "utf8" }
|
|
6046
|
+
);
|
|
6047
|
+
if (updateRef.status !== 0) {
|
|
6048
|
+
const stderr = updateRef.stderr ?? "";
|
|
6049
|
+
throw new Error(
|
|
6050
|
+
`git update-ref ${ref} failed: ${stderr.trim() || "exit " + updateRef.status}`
|
|
6051
|
+
);
|
|
6052
|
+
}
|
|
6053
|
+
return { ref, blob_sha };
|
|
6054
|
+
}
|
|
6055
|
+
function readAttestationRef(patch_id, repoRoot) {
|
|
6056
|
+
const ref = attestationRefName(patch_id);
|
|
6057
|
+
const showRef = spawnSync11(
|
|
6058
|
+
"git",
|
|
6059
|
+
["show-ref", "--verify", "--quiet", ref],
|
|
6060
|
+
{ cwd: repoRoot }
|
|
6061
|
+
);
|
|
6062
|
+
if (showRef.status !== 0) return null;
|
|
6063
|
+
const cat = spawnSync11(
|
|
6064
|
+
"git",
|
|
6065
|
+
["cat-file", "blob", ref],
|
|
6066
|
+
{ cwd: repoRoot, encoding: "buffer", maxBuffer: MAX_PR_ATTESTATION_BYTES * 2 }
|
|
6067
|
+
);
|
|
6068
|
+
if (cat.status !== 0) return null;
|
|
6069
|
+
return parseEnvelope(cat.stdout);
|
|
6070
|
+
}
|
|
6071
|
+
|
|
6072
|
+
// src/commands/attest.ts
|
|
6073
|
+
function runAttest(opts) {
|
|
6074
|
+
const repoRoot = findRepoRoot();
|
|
6075
|
+
const config2 = loadConfig(stampConfigFile(repoRoot));
|
|
6076
|
+
const rule = findBranchRule(config2.branches, opts.into);
|
|
6077
|
+
if (!rule) {
|
|
6078
|
+
throw new Error(
|
|
6079
|
+
`no branch rule for "${opts.into}" in .stamp/config.yml. Configured branches: ${Object.keys(config2.branches).join(", ") || "(none)"}.`
|
|
6080
|
+
);
|
|
6081
|
+
}
|
|
6082
|
+
const branchRef = opts.branch ?? "HEAD";
|
|
6083
|
+
const revspec = `${opts.into}..${branchRef}`;
|
|
6084
|
+
const resolved = resolveDiff(revspec, repoRoot);
|
|
6085
|
+
const db = openDb(stampStateDbPath(repoRoot));
|
|
6086
|
+
let approvals;
|
|
6087
|
+
try {
|
|
6088
|
+
const reviews = latestReviews(db, resolved.base_sha, resolved.head_sha);
|
|
6089
|
+
const byReviewer = new Map(reviews.map((r) => [r.reviewer, r]));
|
|
6090
|
+
const missing = [];
|
|
6091
|
+
for (const r of rule.required) {
|
|
6092
|
+
const rev = byReviewer.get(r);
|
|
6093
|
+
if (!rev || rev.verdict !== "approved") missing.push(r);
|
|
6094
|
+
}
|
|
6095
|
+
if (missing.length > 0) {
|
|
6096
|
+
throw new Error(
|
|
6097
|
+
`gate CLOSED: missing approved verdicts for: ${missing.join(", ")}. Run \`stamp status --diff ${revspec}\` to inspect, then \`stamp review --diff ${revspec}\` to review.`
|
|
6098
|
+
);
|
|
6099
|
+
}
|
|
6100
|
+
approvals = rule.required.map((name) => {
|
|
6101
|
+
const rev = byReviewer.get(name);
|
|
6102
|
+
const toolCalls = redactToolCallsForAttestation(
|
|
6103
|
+
parseToolCalls(rev.tool_calls)
|
|
6104
|
+
);
|
|
6105
|
+
return {
|
|
6106
|
+
reviewer: rev.reviewer,
|
|
6107
|
+
verdict: rev.verdict,
|
|
6108
|
+
review_sha: hashHex(rev.issues ?? ""),
|
|
6109
|
+
...toolCalls.length > 0 ? { tool_calls: toolCalls } : {}
|
|
6110
|
+
};
|
|
6111
|
+
});
|
|
6112
|
+
} finally {
|
|
6113
|
+
db.close();
|
|
6114
|
+
}
|
|
6115
|
+
const baseConfigYaml = showAtRef(
|
|
6116
|
+
resolved.base_sha,
|
|
6117
|
+
".stamp/config.yml",
|
|
6118
|
+
repoRoot
|
|
6119
|
+
);
|
|
6120
|
+
const baseReviewers = readReviewersFromYaml(baseConfigYaml);
|
|
6121
|
+
approvals = approvals.map((a) => {
|
|
6122
|
+
const def = baseReviewers[a.reviewer];
|
|
6123
|
+
if (!def) {
|
|
6124
|
+
throw new Error(
|
|
6125
|
+
`reviewer "${a.reviewer}" approved the diff but is not defined in .stamp/config.yml at base ${resolved.base_sha.slice(0, 8)}. This shouldn't happen \u2014 runReview reads from the same base. File a bug at https://github.com/OpenThinkAi/stamp-cli/issues.`
|
|
6126
|
+
);
|
|
6127
|
+
}
|
|
6128
|
+
const promptText = showAtRef(resolved.base_sha, def.prompt, repoRoot);
|
|
6129
|
+
const source = readReviewerSource2(a.reviewer, repoRoot);
|
|
6130
|
+
return {
|
|
6131
|
+
...a,
|
|
6132
|
+
prompt_sha256: hashPromptBytes(Buffer.from(promptText, "utf8")),
|
|
6133
|
+
tools_sha256: hashTools(def.tools),
|
|
6134
|
+
mcp_sha256: hashMcpServers(def.mcp_servers),
|
|
6135
|
+
...source ? { reviewer_source: source } : {}
|
|
6136
|
+
};
|
|
6137
|
+
});
|
|
6138
|
+
const patch_id = patchIdForSpan(resolved.base_sha, resolved.head_sha, repoRoot);
|
|
6139
|
+
const target_branch_tip_sha = runGit(
|
|
6140
|
+
["rev-parse", `${opts.into}^{commit}`],
|
|
6141
|
+
repoRoot
|
|
6142
|
+
).trim();
|
|
6143
|
+
const { keypair } = ensureUserKeypair();
|
|
6144
|
+
const payload = {
|
|
6145
|
+
schema_version: PR_ATTESTATION_SCHEMA_VERSION,
|
|
6146
|
+
patch_id,
|
|
6147
|
+
base_sha: resolved.base_sha,
|
|
6148
|
+
head_sha: resolved.head_sha,
|
|
6149
|
+
target_branch: opts.into,
|
|
6150
|
+
target_branch_tip_sha,
|
|
6151
|
+
approvals,
|
|
6152
|
+
checks: [],
|
|
6153
|
+
// Phase-1 deliberate omission — see file-level comment.
|
|
6154
|
+
signer_key_id: keypair.fingerprint
|
|
6155
|
+
};
|
|
6156
|
+
const signature = signBytes(keypair.privateKeyPem, serializePayload2(payload));
|
|
6157
|
+
const { ref, blob_sha } = writeAttestationRef(
|
|
6158
|
+
{ payload, signature },
|
|
6159
|
+
repoRoot
|
|
6160
|
+
);
|
|
6161
|
+
const bar = "\u2500".repeat(72);
|
|
6162
|
+
console.log(bar);
|
|
6163
|
+
console.log(`attested ${branchRef} for merge into '${opts.into}'`);
|
|
6164
|
+
console.log(bar);
|
|
6165
|
+
console.log(` patch-id: ${patch_id}`);
|
|
6166
|
+
console.log(
|
|
6167
|
+
` base\u2192head: ${resolved.base_sha.slice(0, 8)} \u2192 ${resolved.head_sha.slice(0, 8)}`
|
|
6168
|
+
);
|
|
6169
|
+
console.log(` signed by: ${keypair.fingerprint}`);
|
|
6170
|
+
console.log(` approvals: ${approvals.map((a) => a.reviewer).join(", ")}`);
|
|
6171
|
+
console.log(` ref: ${ref}`);
|
|
6172
|
+
console.log(` blob: ${blob_sha.slice(0, 12)}`);
|
|
6173
|
+
console.log(bar);
|
|
6174
|
+
if (opts.pushTo) {
|
|
6175
|
+
pushBranchAndAttestation(opts.pushTo, ref, repoRoot);
|
|
6176
|
+
console.log(
|
|
6177
|
+
`
|
|
6178
|
+
\u2713 pushed branch + attestation ref to ${opts.pushTo}. Open the PR; stamp/verify-attestation@v1 will look up refs/stamp/attestations/<patch-id> from your head SHA's diff against the base.`
|
|
6179
|
+
);
|
|
6180
|
+
} else {
|
|
6181
|
+
console.log(
|
|
6182
|
+
`
|
|
6183
|
+
Next: push the branch + attestation ref to your remote, open a PR, and let stamp/verify-attestation@v1 (the GH Action) confirm it. To do both pushes in one shot:
|
|
6184
|
+
|
|
6185
|
+
git push <remote> HEAD ${ref}
|
|
6186
|
+
|
|
6187
|
+
Or re-run with --push <remote> next time.`
|
|
6188
|
+
);
|
|
6189
|
+
}
|
|
6190
|
+
}
|
|
6191
|
+
function pushBranchAndAttestation(remote, attestationRef, repoRoot) {
|
|
6192
|
+
const result = spawnSync12(
|
|
6193
|
+
"git",
|
|
6194
|
+
["push", "--atomic", remote, "HEAD", attestationRef],
|
|
6195
|
+
{ cwd: repoRoot, stdio: "inherit" }
|
|
6196
|
+
);
|
|
6197
|
+
if (result.error) throw result.error;
|
|
6198
|
+
if (result.status !== 0) {
|
|
6199
|
+
const exit = result.status === null ? "(killed by signal)" : `exit ${result.status}`;
|
|
6200
|
+
throw new Error(
|
|
6201
|
+
`git push --atomic ${remote} HEAD ${attestationRef} failed (${exit}). The attestation ref is still in the local repo at ${attestationRef} \u2014 re-run with --push ${remote} after fixing the cause, or push manually.`
|
|
6202
|
+
);
|
|
6203
|
+
}
|
|
6204
|
+
}
|
|
6205
|
+
function hashHex(s) {
|
|
6206
|
+
return createHash7("sha256").update(s, "utf8").digest("hex");
|
|
6207
|
+
}
|
|
6208
|
+
function readReviewerSource2(reviewerName, repoRoot) {
|
|
6209
|
+
const path2 = `.stamp/reviewers/${reviewerName}.lock.json`;
|
|
6210
|
+
if (!pathExistsAtRef("HEAD", path2, repoRoot)) return null;
|
|
6211
|
+
const raw = runGit(["show", `HEAD:${path2}`], repoRoot);
|
|
6212
|
+
try {
|
|
6213
|
+
const parsed = JSON.parse(raw);
|
|
6214
|
+
if (typeof parsed.source === "string" && typeof parsed.ref === "string") {
|
|
6215
|
+
return { source: parsed.source, ref: parsed.ref };
|
|
6216
|
+
}
|
|
6217
|
+
} catch {
|
|
6218
|
+
}
|
|
6219
|
+
return null;
|
|
6220
|
+
}
|
|
6221
|
+
|
|
5861
6222
|
// src/commands/prune.ts
|
|
5862
6223
|
import { existsSync as existsSync16, readdirSync as readdirSync4, statSync as statSync2, unlinkSync as unlinkSync3 } from "fs";
|
|
5863
6224
|
import { join as join10 } from "path";
|
|
@@ -6123,7 +6484,7 @@ function loadOrEmpty() {
|
|
|
6123
6484
|
}
|
|
6124
6485
|
|
|
6125
6486
|
// src/commands/reviewers.ts
|
|
6126
|
-
import { spawnSync as
|
|
6487
|
+
import { spawnSync as spawnSync13 } from "child_process";
|
|
6127
6488
|
import {
|
|
6128
6489
|
existsSync as existsSync19,
|
|
6129
6490
|
readFileSync as readFileSync13,
|
|
@@ -6767,7 +7128,7 @@ function buildConfigYamlHint(reviewerName, tools, mcpServers) {
|
|
|
6767
7128
|
}
|
|
6768
7129
|
function launchEditor(path2) {
|
|
6769
7130
|
const editor = process.env["EDITOR"] ?? process.env["VISUAL"] ?? (process.platform === "win32" ? "notepad" : "vi");
|
|
6770
|
-
const result =
|
|
7131
|
+
const result = spawnSync13(editor, [path2], { stdio: "inherit" });
|
|
6771
7132
|
if (result.error) {
|
|
6772
7133
|
throw new Error(
|
|
6773
7134
|
`failed to launch editor "${editor}": ${result.error.message}`
|
|
@@ -6851,14 +7212,14 @@ function printGate(result, base_sha, head_sha) {
|
|
|
6851
7212
|
}
|
|
6852
7213
|
|
|
6853
7214
|
// src/commands/update.ts
|
|
6854
|
-
import { spawnSync as
|
|
7215
|
+
import { spawnSync as spawnSync14 } from "child_process";
|
|
6855
7216
|
|
|
6856
7217
|
// src/lib/version.ts
|
|
6857
7218
|
import { readFileSync as readFileSync14 } from "fs";
|
|
6858
|
-
import { dirname as
|
|
7219
|
+
import { dirname as dirname6, join as join13 } from "path";
|
|
6859
7220
|
import { fileURLToPath } from "url";
|
|
6860
7221
|
function readPackageVersion() {
|
|
6861
|
-
const here =
|
|
7222
|
+
const here = dirname6(fileURLToPath(import.meta.url));
|
|
6862
7223
|
for (let dir = here, i = 0; i < 6; i++) {
|
|
6863
7224
|
try {
|
|
6864
7225
|
const raw = readFileSync14(join13(dir, "package.json"), "utf8");
|
|
@@ -6866,7 +7227,7 @@ function readPackageVersion() {
|
|
|
6866
7227
|
if (pkg.name === "@openthink/stamp" && pkg.version) return pkg.version;
|
|
6867
7228
|
} catch {
|
|
6868
7229
|
}
|
|
6869
|
-
const parent =
|
|
7230
|
+
const parent = dirname6(dir);
|
|
6870
7231
|
if (parent === dir) break;
|
|
6871
7232
|
dir = parent;
|
|
6872
7233
|
}
|
|
@@ -6892,7 +7253,7 @@ function runUpdate() {
|
|
|
6892
7253
|
`);
|
|
6893
7254
|
process.stdout.write(`checking npm registry for latest...
|
|
6894
7255
|
`);
|
|
6895
|
-
const viewResult =
|
|
7256
|
+
const viewResult = spawnSync14("npm", ["view", PKG_NAME, "version"], {
|
|
6896
7257
|
encoding: "utf8"
|
|
6897
7258
|
});
|
|
6898
7259
|
if (viewResult.error || viewResult.status !== 0) {
|
|
@@ -6926,7 +7287,7 @@ function runUpdate() {
|
|
|
6926
7287
|
}
|
|
6927
7288
|
process.stdout.write(`installing ${PKG_NAME}@${latest}...
|
|
6928
7289
|
`);
|
|
6929
|
-
const installResult =
|
|
7290
|
+
const installResult = spawnSync14(
|
|
6930
7291
|
"npm",
|
|
6931
7292
|
["install", "-g", `${PKG_NAME}@${latest}`],
|
|
6932
7293
|
{ stdio: "inherit" }
|
|
@@ -6947,9 +7308,9 @@ through that tool instead \u2014 this command only uses 'npm install -g'.`
|
|
|
6947
7308
|
}
|
|
6948
7309
|
|
|
6949
7310
|
// src/commands/verify.ts
|
|
6950
|
-
import { execFileSync as execFileSync2, spawnSync as
|
|
7311
|
+
import { execFileSync as execFileSync2, spawnSync as spawnSync15 } from "child_process";
|
|
6951
7312
|
function loadConfigAtSha(sha, repoRoot) {
|
|
6952
|
-
const result =
|
|
7313
|
+
const result = spawnSync15(
|
|
6953
7314
|
"git",
|
|
6954
7315
|
["show", `${sha}:.stamp/config.yml`],
|
|
6955
7316
|
{ cwd: repoRoot, encoding: "utf8", maxBuffer: 16 * 1024 * 1024 }
|
|
@@ -7159,6 +7520,194 @@ function git2(args, cwd) {
|
|
|
7159
7520
|
}
|
|
7160
7521
|
}
|
|
7161
7522
|
|
|
7523
|
+
// src/commands/verifyPr.ts
|
|
7524
|
+
import { spawnSync as spawnSync16 } from "child_process";
|
|
7525
|
+
function runVerifyPr(opts) {
|
|
7526
|
+
const repoRoot = opts.repoPath ?? findRepoRoot();
|
|
7527
|
+
const resolved = resolveDiff(`${opts.base}..${opts.head}`, repoRoot);
|
|
7528
|
+
const patch_id = patchIdForSpan(resolved.base_sha, resolved.head_sha, repoRoot);
|
|
7529
|
+
const envelope = readAttestationRef(patch_id, repoRoot);
|
|
7530
|
+
if (!envelope) {
|
|
7531
|
+
fail2(
|
|
7532
|
+
`no attestation found at refs/stamp/attestations/${patch_id} (diff ${resolved.base_sha.slice(0, 8)}..${resolved.head_sha.slice(0, 8)}). Operator must run \`stamp attest --into ${opts.into}\` and push the attestation ref to this remote.`,
|
|
7533
|
+
patch_id,
|
|
7534
|
+
resolved.base_sha,
|
|
7535
|
+
resolved.head_sha
|
|
7536
|
+
);
|
|
7537
|
+
}
|
|
7538
|
+
const { payload, signature } = envelope;
|
|
7539
|
+
const trustedKey = findTrustedKeyAtBase(
|
|
7540
|
+
resolved.base_sha,
|
|
7541
|
+
payload.signer_key_id,
|
|
7542
|
+
repoRoot
|
|
7543
|
+
);
|
|
7544
|
+
if (!trustedKey) {
|
|
7545
|
+
fail2(
|
|
7546
|
+
`signer_key_id ${payload.signer_key_id} is not in .stamp/trusted-keys/ at base ${resolved.base_sha.slice(0, 8)}. The reviewer's signing key must be added to this repo's trust set via \`stamp trust grant\` and landed through the standard stamp gate before their attestations verify.`,
|
|
7547
|
+
patch_id,
|
|
7548
|
+
resolved.base_sha,
|
|
7549
|
+
resolved.head_sha
|
|
7550
|
+
);
|
|
7551
|
+
}
|
|
7552
|
+
const sigOk = verifyBytes(
|
|
7553
|
+
trustedKey.pem,
|
|
7554
|
+
serializePayload2(payload),
|
|
7555
|
+
signature
|
|
7556
|
+
);
|
|
7557
|
+
if (!sigOk) {
|
|
7558
|
+
fail2(
|
|
7559
|
+
`signature verification failed against ${trustedKey.filename} (${payload.signer_key_id}). Either the attestation has been tampered with after signing, or the trusted-keys entry doesn't match the key that signed.`,
|
|
7560
|
+
patch_id,
|
|
7561
|
+
resolved.base_sha,
|
|
7562
|
+
resolved.head_sha
|
|
7563
|
+
);
|
|
7564
|
+
}
|
|
7565
|
+
if (payload.target_branch !== opts.into) {
|
|
7566
|
+
fail2(
|
|
7567
|
+
`attestation target_branch="${payload.target_branch}" does not match verifier --into="${opts.into}". The reviewer signed an attestation for a different merge destination \u2014 re-attest with --into ${opts.into}.`,
|
|
7568
|
+
patch_id,
|
|
7569
|
+
resolved.base_sha,
|
|
7570
|
+
resolved.head_sha
|
|
7571
|
+
);
|
|
7572
|
+
}
|
|
7573
|
+
let configYaml;
|
|
7574
|
+
try {
|
|
7575
|
+
configYaml = showAtRef(resolved.base_sha, ".stamp/config.yml", repoRoot);
|
|
7576
|
+
} catch (e) {
|
|
7577
|
+
fail2(
|
|
7578
|
+
`could not read .stamp/config.yml at base ${resolved.base_sha.slice(0, 8)}: ${e.message}`,
|
|
7579
|
+
patch_id,
|
|
7580
|
+
resolved.base_sha,
|
|
7581
|
+
resolved.head_sha
|
|
7582
|
+
);
|
|
7583
|
+
}
|
|
7584
|
+
const config2 = parseConfigFromYaml(configYaml);
|
|
7585
|
+
const rule = findBranchRule(config2.branches, opts.into);
|
|
7586
|
+
if (!rule) {
|
|
7587
|
+
fail2(
|
|
7588
|
+
`no branch rule for "${opts.into}" in .stamp/config.yml at base ${resolved.base_sha.slice(0, 8)}. Configured branches: ${Object.keys(config2.branches).join(", ") || "(none)"}.`,
|
|
7589
|
+
patch_id,
|
|
7590
|
+
resolved.base_sha,
|
|
7591
|
+
resolved.head_sha
|
|
7592
|
+
);
|
|
7593
|
+
}
|
|
7594
|
+
const approvalByReviewer = new Map(
|
|
7595
|
+
payload.approvals.map((a) => [a.reviewer, a.verdict])
|
|
7596
|
+
);
|
|
7597
|
+
const missing = [];
|
|
7598
|
+
for (const r of rule.required) {
|
|
7599
|
+
if (approvalByReviewer.get(r) !== "approved") missing.push(r);
|
|
7600
|
+
}
|
|
7601
|
+
if (missing.length > 0) {
|
|
7602
|
+
fail2(
|
|
7603
|
+
`gate CLOSED: missing approved verdicts for: ${missing.join(", ")}. Attestation has approvals for: ${payload.approvals.map((a) => `${a.reviewer}=${a.verdict}`).join(", ")}.`,
|
|
7604
|
+
patch_id,
|
|
7605
|
+
resolved.base_sha,
|
|
7606
|
+
resolved.head_sha
|
|
7607
|
+
);
|
|
7608
|
+
}
|
|
7609
|
+
if (rule.strict_base) {
|
|
7610
|
+
if (!payload.target_branch_tip_sha) {
|
|
7611
|
+
fail2(
|
|
7612
|
+
`strict_base check failed: attestation schema v${payload.schema_version} predates target_branch_tip_sha (v2+). Re-attest with stamp \u2265 1.6.0 to verify under strict_base; or relax the branch rule.`,
|
|
7613
|
+
patch_id,
|
|
7614
|
+
resolved.base_sha,
|
|
7615
|
+
resolved.head_sha
|
|
7616
|
+
);
|
|
7617
|
+
}
|
|
7618
|
+
const currentTip = runGit(
|
|
7619
|
+
["rev-parse", `${opts.into}^{commit}`],
|
|
7620
|
+
repoRoot
|
|
7621
|
+
).trim();
|
|
7622
|
+
if (payload.target_branch_tip_sha !== currentTip) {
|
|
7623
|
+
fail2(
|
|
7624
|
+
`strict_base check failed: attestation was signed when ${opts.into} was at ${payload.target_branch_tip_sha.slice(0, 8)}, but ${opts.into} is now at ${currentTip.slice(0, 8)}. Re-attest with the current tip.`,
|
|
7625
|
+
patch_id,
|
|
7626
|
+
resolved.base_sha,
|
|
7627
|
+
resolved.head_sha
|
|
7628
|
+
);
|
|
7629
|
+
}
|
|
7630
|
+
}
|
|
7631
|
+
printSuccess3({
|
|
7632
|
+
patch_id,
|
|
7633
|
+
base_sha: resolved.base_sha,
|
|
7634
|
+
head_sha: resolved.head_sha,
|
|
7635
|
+
target_branch: opts.into,
|
|
7636
|
+
signer_key_id: payload.signer_key_id,
|
|
7637
|
+
trusted_key_filename: trustedKey.filename,
|
|
7638
|
+
approvals: payload.approvals.map((a) => ({
|
|
7639
|
+
reviewer: a.reviewer,
|
|
7640
|
+
verdict: a.verdict
|
|
7641
|
+
})),
|
|
7642
|
+
strict_base: rule.strict_base ?? false
|
|
7643
|
+
});
|
|
7644
|
+
}
|
|
7645
|
+
function findTrustedKeyAtBase(base_sha, signer_key_id, repoRoot) {
|
|
7646
|
+
const lsTree = spawnSync16(
|
|
7647
|
+
"git",
|
|
7648
|
+
[
|
|
7649
|
+
"ls-tree",
|
|
7650
|
+
"--name-only",
|
|
7651
|
+
"-r",
|
|
7652
|
+
base_sha,
|
|
7653
|
+
".stamp/trusted-keys/"
|
|
7654
|
+
],
|
|
7655
|
+
{ cwd: repoRoot, encoding: "utf8" }
|
|
7656
|
+
);
|
|
7657
|
+
if (lsTree.status !== 0) {
|
|
7658
|
+
return null;
|
|
7659
|
+
}
|
|
7660
|
+
const files = (lsTree.stdout ?? "").split("\n").map((l) => l.trim()).filter((l) => l.endsWith(".pub"));
|
|
7661
|
+
for (const file of files) {
|
|
7662
|
+
let pem;
|
|
7663
|
+
try {
|
|
7664
|
+
pem = runGit(["show", `${base_sha}:${file}`], repoRoot);
|
|
7665
|
+
} catch {
|
|
7666
|
+
continue;
|
|
7667
|
+
}
|
|
7668
|
+
let fp;
|
|
7669
|
+
try {
|
|
7670
|
+
fp = fingerprintFromPem(pem);
|
|
7671
|
+
} catch {
|
|
7672
|
+
continue;
|
|
7673
|
+
}
|
|
7674
|
+
if (fp === signer_key_id) {
|
|
7675
|
+
return { filename: file.replace(/^\.stamp\/trusted-keys\//, ""), pem };
|
|
7676
|
+
}
|
|
7677
|
+
}
|
|
7678
|
+
return null;
|
|
7679
|
+
}
|
|
7680
|
+
function printSuccess3(s) {
|
|
7681
|
+
const bar = "\u2500".repeat(72);
|
|
7682
|
+
console.log(bar);
|
|
7683
|
+
console.log(
|
|
7684
|
+
`target: ${s.target_branch} base: ${s.base_sha.slice(0, 8)} \u2192 head: ${s.head_sha.slice(0, 8)}`
|
|
7685
|
+
);
|
|
7686
|
+
console.log(bar);
|
|
7687
|
+
console.log(` patch-id: ${s.patch_id}`);
|
|
7688
|
+
console.log(` signer: ${s.signer_key_id}`);
|
|
7689
|
+
console.log(` trusted-key: .stamp/trusted-keys/${s.trusted_key_filename}`);
|
|
7690
|
+
console.log(` base mode: ${s.strict_base ? "strict" : "loose"}`);
|
|
7691
|
+
for (const a of s.approvals) {
|
|
7692
|
+
const mark = a.verdict === "approved" ? "\u2713" : "\u2717";
|
|
7693
|
+
console.log(` ${mark} ${a.reviewer.padEnd(16)} ${a.verdict}`);
|
|
7694
|
+
}
|
|
7695
|
+
console.log(bar);
|
|
7696
|
+
console.log("result: VERIFIED");
|
|
7697
|
+
console.log(bar);
|
|
7698
|
+
}
|
|
7699
|
+
function fail2(reason, patch_id, base_sha, head_sha) {
|
|
7700
|
+
const bar = "\u2500".repeat(72);
|
|
7701
|
+
console.error(bar);
|
|
7702
|
+
console.error(`base: ${base_sha.slice(0, 8)} \u2192 head: ${head_sha.slice(0, 8)}`);
|
|
7703
|
+
console.error(`patch-id: ${patch_id}`);
|
|
7704
|
+
console.error(bar);
|
|
7705
|
+
console.error(`error: ${reason}`);
|
|
7706
|
+
console.error("result: FAILED");
|
|
7707
|
+
console.error(bar);
|
|
7708
|
+
process.exit(1);
|
|
7709
|
+
}
|
|
7710
|
+
|
|
7162
7711
|
// src/index.ts
|
|
7163
7712
|
process.removeAllListeners("warning");
|
|
7164
7713
|
process.on("warning", (warn) => {
|
|
@@ -7198,6 +7747,9 @@ program.command("init").description(
|
|
|
7198
7747
|
).option(
|
|
7199
7748
|
"--no-oteam",
|
|
7200
7749
|
"bypass the oteam-detection prompt that offers to fill stamp.host in ~/.open-team/config.json"
|
|
7750
|
+
).option(
|
|
7751
|
+
"--no-pr-check",
|
|
7752
|
+
"skip dropping .github/workflows/stamp-verify.yml (PR-check mode workflow). Default behaviour: write the workflow for forge-direct + local-only modes, skip for server-gated."
|
|
7201
7753
|
).action(
|
|
7202
7754
|
(opts) => {
|
|
7203
7755
|
try {
|
|
@@ -7219,7 +7771,12 @@ program.command("init").description(
|
|
|
7219
7771
|
ghProtect: opts.ghProtect,
|
|
7220
7772
|
mode,
|
|
7221
7773
|
remote: opts.remote,
|
|
7222
|
-
oteam: opts.oteam
|
|
7774
|
+
oteam: opts.oteam,
|
|
7775
|
+
// commander's --no-pr-check yields opts.prCheck === false; no
|
|
7776
|
+
// flag yields true (the default-true sentinel). We only want
|
|
7777
|
+
// to forward an explicit `false` to runInit so its mode-aware
|
|
7778
|
+
// default fires when the operator hasn't opted out.
|
|
7779
|
+
prCheck: opts.prCheck === false ? false : void 0
|
|
7223
7780
|
});
|
|
7224
7781
|
} catch (err) {
|
|
7225
7782
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -7473,6 +8030,21 @@ program.command("merge <branch>").description("merge <branch> into --into <targe
|
|
|
7473
8030
|
handleCliError(err);
|
|
7474
8031
|
}
|
|
7475
8032
|
});
|
|
8033
|
+
program.command("attest [branch]").description(
|
|
8034
|
+
"PR-check mode counterpart to `stamp merge` \u2014 sign an attestation envelope and write it to refs/stamp/attestations/<patch-id> for a GitHub Action to verify on the PR (no actual git merge happens here)"
|
|
8035
|
+
).requiredOption("--into <target>", "target branch whose rule the gate is checked against").option(
|
|
8036
|
+
"--push [remote]",
|
|
8037
|
+
"after attesting locally, push the current branch + the attestation ref to <remote> in one atomic git push (default remote: origin)"
|
|
8038
|
+
).action(
|
|
8039
|
+
(branch, opts) => {
|
|
8040
|
+
try {
|
|
8041
|
+
const pushTo = opts.push === true ? "origin" : typeof opts.push === "string" ? opts.push : void 0;
|
|
8042
|
+
runAttest({ branch, into: opts.into, pushTo });
|
|
8043
|
+
} catch (err) {
|
|
8044
|
+
handleCliError(err);
|
|
8045
|
+
}
|
|
8046
|
+
}
|
|
8047
|
+
);
|
|
7476
8048
|
program.command("push <target>").description("push <target> to origin; surfaces stamp-verify hook stderr on rejection").option("--remote <name>", "remote to push to", "origin").action((target, opts) => {
|
|
7477
8049
|
try {
|
|
7478
8050
|
runPush({ target, remote: opts.remote });
|
|
@@ -7487,6 +8059,18 @@ program.command("verify <sha>").description("verify an existing merge commit's a
|
|
|
7487
8059
|
handleCliError(err);
|
|
7488
8060
|
}
|
|
7489
8061
|
});
|
|
8062
|
+
program.command("verify-pr <head>").description(
|
|
8063
|
+
"verify a PR attestation at refs/stamp/attestations/<patch-id> for the diff <base>..<head> against the --into branch's rule (used by stamp/verify-attestation@v1; also runnable locally for debugging)"
|
|
8064
|
+
).requiredOption("--base <ref>", "PR base ref (commit SHA, branch, or any rev-parse-able value)").requiredOption(
|
|
8065
|
+
"--into <branch>",
|
|
8066
|
+
"branch the PR is merging into; must equal the attestation's target_branch"
|
|
8067
|
+
).action((head, opts) => {
|
|
8068
|
+
try {
|
|
8069
|
+
runVerifyPr({ head, base: opts.base, into: opts.into });
|
|
8070
|
+
} catch (err) {
|
|
8071
|
+
handleCliError(err);
|
|
8072
|
+
}
|
|
8073
|
+
});
|
|
7490
8074
|
program.command("update").description(
|
|
7491
8075
|
"upgrade stamp to the latest npm release (runs 'npm install -g @openthink/stamp@latest')"
|
|
7492
8076
|
).action(() => wrap(() => runUpdate()));
|