Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ jobs:
- id: forward-roll-baseline
name: Resolve the published forward-roll baseline
if: steps.plan.outputs.state_root_compat == 'true'
run: node scripts/release-cli-publication.mjs resolve-nightly-predecessor "$GITHUB_OUTPUT"
run: node scripts/release-cli-publication.mjs resolve-nightly-predecessor "$GITHUB_OUTPUT" HEAD

- name: Download the forward-roll baseline
if: steps.plan.outputs.state_root_compat == 'true'
Expand Down
15 changes: 13 additions & 2 deletions .github/workflows/cli-package-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ on:
- 'packages/storage/src/native-file-lock.ts'
- 'scripts/generate-runtime-host-peer-*'
- 'scripts/release-cli-package.mjs'
- 'scripts/release-cli-publication.mjs'
- 'scripts/release-cli-publication.test.mjs'
- 'scripts/release-cli-workflow-policy.test.mjs'
- 'scripts/qualify-released-cli-state-root.mjs'
- 'scripts/qualify-released-cli-state-root.test.mjs'
- 'scripts/released-cli-state-root-fixture.mjs'
Expand Down Expand Up @@ -76,6 +79,9 @@ on:
release_predecessor_integrity:
description: npm SHA-512 integrity of the Nightly tarball qualified against this candidate
value: ${{ jobs.state-root-qualification.outputs.release_predecessor_integrity }}
release_predecessor_source_commit:
description: Source commit proven for the npm Nightly predecessor
value: ${{ jobs.state-root-qualification.outputs.release_predecessor_source_commit }}
workflow_dispatch:

permissions:
Expand Down Expand Up @@ -339,10 +345,12 @@ jobs:
release_predecessor_version: ${{ steps.predecessor.outputs.version }}
release_predecessor_tarball_url: ${{ steps.predecessor.outputs.tarball_url }}
release_predecessor_integrity: ${{ steps.predecessor.outputs.integrity }}
release_predecessor_source_commit: ${{ steps.predecessor.outputs.source_commit }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.source_commit || github.sha }}
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
Expand All @@ -366,7 +374,7 @@ jobs:
# is now this job's output.
- name: Resolve the current npm Nightly as immutable evidence
id: predecessor
run: node scripts/release-cli-publication.mjs resolve-nightly-predecessor "$GITHUB_OUTPUT"
run: node scripts/release-cli-publication.mjs resolve-nightly-predecessor "$GITHUB_OUTPUT" HEAD
# Three runs of one script against one sandbox, not three runners. Two of
# these transitions are between tarballs that were published and frozen,
# so nothing in a pull request can change their outcome except the
Expand All @@ -379,6 +387,7 @@ jobs:
MAKA_QUALIFICATION_BWRAP_USE_SUDO: '1'
PREDECESSOR_TARBALL_URL: ${{ steps.predecessor.outputs.tarball_url }}
PREDECESSOR_INTEGRITY: ${{ steps.predecessor.outputs.integrity }}
PREDECESSOR_SOURCE_COMMIT: ${{ steps.predecessor.outputs.source_commit }}
run: |
set -euo pipefail
evidence_root="$RUNNER_TEMP/released-state-root"
Expand Down Expand Up @@ -458,11 +467,13 @@ jobs:
PREDECESSOR_VERSION: ${{ steps.predecessor.outputs.version }}
PREDECESSOR_TARBALL_URL: ${{ steps.predecessor.outputs.tarball_url }}
PREDECESSOR_INTEGRITY: ${{ steps.predecessor.outputs.integrity }}
PREDECESSOR_SOURCE_COMMIT: ${{ steps.predecessor.outputs.source_commit }}
run: |
node scripts/release-cli-publication.mjs assert-nightly-predecessor \
"$PREDECESSOR_VERSION" \
"$PREDECESSOR_TARBALL_URL" \
"$PREDECESSOR_INTEGRITY"
"$PREDECESSOR_INTEGRITY" \
"$PREDECESSOR_SOURCE_COMMIT"

eval:
name: Validate installed CLI Eval
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/npm-publication.yml
Original file line number Diff line number Diff line change
Expand Up @@ -153,11 +153,13 @@ jobs:
PREDECESSOR_VERSION: ${{ needs.cli.outputs.release_predecessor_version }}
PREDECESSOR_TARBALL_URL: ${{ needs.cli.outputs.release_predecessor_tarball_url }}
PREDECESSOR_INTEGRITY: ${{ needs.cli.outputs.release_predecessor_integrity }}
PREDECESSOR_SOURCE_COMMIT: ${{ needs.cli.outputs.release_predecessor_source_commit }}
run: |
node scripts/release-cli-publication.mjs assert-nightly-predecessor \
"$PREDECESSOR_VERSION" \
"$PREDECESSOR_TARBALL_URL" \
"$PREDECESSOR_INTEGRITY"
"$PREDECESSOR_INTEGRITY" \
"$PREDECESSOR_SOURCE_COMMIT"
current="$(npm view maka-agent dist-tags.nightly --registry https://registry.npmjs.org/)"
node scripts/product-nightly.mjs assert-channel-advance "$NIGHTLY_VERSION" "$current"

Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/release-cli-stage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ jobs:
PREDECESSOR_VERSION: ${{ needs.validate.outputs.release_predecessor_version }}
PREDECESSOR_TARBALL_URL: ${{ needs.validate.outputs.release_predecessor_tarball_url }}
PREDECESSOR_INTEGRITY: ${{ needs.validate.outputs.release_predecessor_integrity }}
PREDECESSOR_SOURCE_COMMIT: ${{ needs.validate.outputs.release_predecessor_source_commit }}
PRODUCT_SOURCE_COMMIT: ${{ needs.authorize.outputs.source_commit }}
PRODUCT_TAG: ${{ needs.authorize.outputs.product_tag }}
RELEASE_DIST_TAG: ${{ steps.release.outputs.dist_tag }}
Expand All @@ -186,7 +187,8 @@ jobs:
node scripts/release-cli-publication.mjs assert-nightly-predecessor \
"$PREDECESSOR_VERSION" \
"$PREDECESSOR_TARBALL_URL" \
"$PREDECESSOR_INTEGRITY"
"$PREDECESSOR_INTEGRITY" \
"$PREDECESSOR_SOURCE_COMMIT"

node scripts/product-release-authority.mjs verify-draft \
"$PRODUCT_TAG" "$PRODUCT_SOURCE_COMMIT" "$GITHUB_REPOSITORY"
Expand Down
132 changes: 123 additions & 9 deletions scripts/release-cli-publication.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@
* under the License.
*/

import { execFileSync } from 'node:child_process';
import { appendFileSync, copyFileSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { basename, join, resolve } from 'node:path';
import { basename, dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { createHash } from 'node:crypto';
import { CLI_RELEASE_ARTIFACT_LIMITS } from './release-cli-artifact-policy.mjs';
Expand All @@ -30,9 +31,16 @@ import {

const PACKAGE_NAME = 'maka-agent';
const REGISTRY_ORIGIN = 'https://registry.npmjs.org';
const REGISTRY_ATTESTATION_PATH = `${REGISTRY_ORIGIN}/-/npm/v1/attestations`;
const REPOSITORY = 'apache/maka';
const PUBLICATION_WORKFLOW_PATH = '.github/workflows/npm-publication.yml';
const REGISTRY_REQUEST_TIMEOUT_MS = 30_000;
const DEFAULT_REPO_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
const SLSA_PROVENANCE_PREDICATE = 'https://slsa.dev/provenance/v1';
const SLSA_WORKFLOW_BUILD_TYPE =
'https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1';
const REPOSITORY_URL = `https://github.com/${REPOSITORY}`;
const MAIN_REF = 'refs/heads/main';
const RELEASE_RECORD_KEYS = [
'schemaVersion',
'packageName',
Expand Down Expand Up @@ -218,7 +226,92 @@ export async function fetchRegistryRelease({
return { ...record, tarballPath, sha256 };
}

export async function resolveRegistryNightlyPredecessor({ fetchImpl = fetch } = {}) {
export function assertCommitIsAncestor({
commit,
head = 'HEAD',
repoRoot = DEFAULT_REPO_ROOT,
exec = execFileSync,
}) {
if (typeof commit !== 'string' || !/^[0-9a-f]{40}$/iu.test(commit)) {
throw new Error(`Invalid git commit SHA for ancestor check: ${commit}`);
}
try {
exec('git', ['merge-base', '--is-ancestor', commit, head], {
cwd: repoRoot,
stdio: ['ignore', 'ignore', 'pipe'],
});
} catch (error) {
if (error?.status === 1) {
throw new Error(
`Registry Nightly source commit ${commit} is not an ancestor of ${head}; forward-roll baseline must precede the change under test (#4447)`,
);
}
throw new Error(
`Unable to verify that Registry Nightly source commit ${commit} is an ancestor of ${head}; the checkout history or commit is unavailable (#4447)`,
{ cause: error },
);
}
}

export function parseRegistryNightlySourceCommit({ version, attestations }) {
// Nightlies are published from tarballs, so npm leaves versionMetadata.gitHead
// empty. SLSA provenance is the publisher-signed source identity we can fence.
if (!Array.isArray(attestations)) {
throw new Error(`Registry Nightly ${version} has no valid provenance attestations`);
}
const expectedSubject = `pkg:npm/${PACKAGE_NAME}@${version}`;
const expectedSourceUri = `git+${REPOSITORY_URL}@${MAIN_REF}`;
const statement = attestations.map(parseProvenanceStatement).find((candidate) => {
const definition = candidate?.predicate?.buildDefinition;
const workflow = definition?.externalParameters?.workflow;
const dependencies = definition?.resolvedDependencies;
return (
candidate?._type === 'https://in-toto.io/Statement/v1' &&
candidate?.predicateType === SLSA_PROVENANCE_PREDICATE &&
candidate?.subject?.some((subject) => subject?.name === expectedSubject) &&
definition?.buildType === SLSA_WORKFLOW_BUILD_TYPE &&
workflow?.repository === REPOSITORY_URL &&
workflow?.ref === MAIN_REF &&
workflow?.path === PUBLICATION_WORKFLOW_PATH &&
['schedule', 'workflow_dispatch'].includes(
definition?.internalParameters?.github?.event_name,
) &&
Array.isArray(dependencies) &&
dependencies.some(
(dependency) =>
dependency?.uri === expectedSourceUri &&
/^[0-9a-f]{40}$/iu.test(dependency?.digest?.gitCommit ?? ''),
)
);
});
const sourceCommit = statement?.predicate?.buildDefinition?.resolvedDependencies?.find(
(dependency) => dependency?.uri === expectedSourceUri,
)?.digest?.gitCommit;
if (!sourceCommit) {
throw new Error(
`Registry Nightly ${version} has no valid SLSA source commit for ${REPOSITORY}@${MAIN_REF}`,
);
}
return sourceCommit;
}

async function fetchRegistryNightlySourceCommit({ version, fetchImpl }) {
const attestations = await fetchJson(
fetchImpl,
`${REGISTRY_ATTESTATION_PATH}/${PACKAGE_NAME}@${version}`,
'Nightly provenance attestations',
'application/json',
);
return parseRegistryNightlySourceCommit({ version, attestations: attestations.attestations });
}

export async function resolveRegistryNightlyPredecessor({
fetchImpl = fetch,
fencedAncestorHead,
includeSourceCommit = fencedAncestorHead !== undefined,
repoRoot = DEFAULT_REPO_ROOT,
exec = execFileSync,
} = {}) {
const packageMetadata = await fetchJson(
fetchImpl,
`${REGISTRY_ORIGIN}/${PACKAGE_NAME}`,
Expand All @@ -237,27 +330,46 @@ export async function resolveRegistryNightlyPredecessor({ fetchImpl = fetch } =
throw new Error('Registry Nightly identity does not match its dist-tag');
}

let sourceCommit;
if (includeSourceCommit) {
sourceCommit = await fetchRegistryNightlySourceCommit({ version, fetchImpl });
if (fencedAncestorHead !== undefined) {
assertCommitIsAncestor({
commit: sourceCommit,
head: fencedAncestorHead,
repoRoot,
exec,
});
}
}

const tarball = `${PACKAGE_NAME}-${version}.tgz`;
const tarballUrl = parseRegistryTarballUrl(versionMetadata.dist?.tarball, tarball);
const integrity = parseSha512Integrity(versionMetadata.dist?.integrity);
return {
version,
tarballUrl,
integrity,
...(sourceCommit ? { sourceCommit } : {}),
};
}

export async function assertRegistryNightlyPredecessor({
expectedVersion,
expectedTarballUrl,
expectedIntegrity,
expectedSourceCommit,
fetchImpl = fetch,
}) {
const current = await resolveRegistryNightlyPredecessor({ fetchImpl });
const current = await resolveRegistryNightlyPredecessor({
fetchImpl,
includeSourceCommit: expectedSourceCommit !== undefined,
});
if (
current.version !== expectedVersion ||
current.tarballUrl !== expectedTarballUrl ||
current.integrity !== expectedIntegrity
current.integrity !== expectedIntegrity ||
current.sourceCommit !== expectedSourceCommit
) {
throw new Error(
`Qualified npm Nightly predecessor ${expectedVersion} is no longer current; found ${current.version}`,
Expand Down Expand Up @@ -621,22 +733,24 @@ async function main() {
});
return;
}
if (command === 'resolve-nightly-predecessor' && args.length === 1) {
const [output] = args;
const predecessor = await resolveRegistryNightlyPredecessor();
if (command === 'resolve-nightly-predecessor' && (args.length === 1 || args.length === 2)) {
const [output, fencedAncestorHead] = args;
const predecessor = await resolveRegistryNightlyPredecessor({ fencedAncestorHead });
appendOutputs(output, {
version: predecessor.version,
tarball_url: predecessor.tarballUrl,
integrity: predecessor.integrity,
...(predecessor.sourceCommit ? { source_commit: predecessor.sourceCommit } : {}),
});
return;
}
if (command === 'assert-nightly-predecessor' && args.length === 3) {
const [expectedVersion, expectedTarballUrl, expectedIntegrity] = args;
if (command === 'assert-nightly-predecessor' && (args.length === 3 || args.length === 4)) {
const [expectedVersion, expectedTarballUrl, expectedIntegrity, expectedSourceCommit] = args;
await assertRegistryNightlyPredecessor({
expectedVersion,
expectedTarballUrl,
expectedIntegrity,
expectedSourceCommit,
});
return;
}
Expand Down
Loading