Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
3421731
fix(release): publish authenticated Mono native artifacts
haasonsaas Sep 5, 2026
ba11387
fix(release): serialize equivalent version dispatches
haasonsaas Sep 5, 2026
3fc5658
fix(release): require protected public source before finalization
haasonsaas Sep 5, 2026
38fd705
fix(release): fetch history needed for source ancestry checks
haasonsaas Sep 5, 2026
99ace6c
fix(release): require authenticated source projection manifest
haasonsaas Sep 5, 2026
edf38e2
fix(release): enforce authenticated handoff workflow contracts
haasonsaas Sep 5, 2026
d44b302
Merge remote-tracking branch 'origin/main' into fix/consume-signed-mo…
haasonsaas Sep 5, 2026
5bfbf77
fix(release): dispatch staged tag retries from main
haasonsaas Sep 5, 2026
4578226
fix(release): preserve signed optional device capability
haasonsaas Sep 5, 2026
0d30c50
fix(release): authenticate capability markers for older source tags
haasonsaas Sep 5, 2026
4620704
test(release): accept only strict shell preamble before authentication
haasonsaas Sep 5, 2026
f1c6481
test(release): execute missing and altered marker checks
haasonsaas Sep 5, 2026
5b510b3
test(release): run marker authentication cases in required CI
haasonsaas Sep 5, 2026
7534d28
fix(release): reject stale disabled helper archives before publication
haasonsaas Sep 5, 2026
c061ffb
Merge reviewed v0.10.74 public source into signed release publisher
haasonsaas Sep 6, 2026
2c55219
chore: sync public mirror from internal
github-actions[bot] Sep 6, 2026
006b131
Merge verified Gradle stdin fix from generated public source
haasonsaas Sep 6, 2026
2a610bb
Merge protected public main into signed release publisher
haasonsaas Sep 6, 2026
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
60 changes: 44 additions & 16 deletions .github/workflows/channel-release-contract.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,65 @@ import { test } from "node:test";
const workflow = await readFile(new URL("./channel-release.yml", import.meta.url), "utf8");
const releaseWorkflow = await readFile(new URL("./release.yml", import.meta.url), "utf8");

test("preview schedules keep beta behind alpha", () => {
assert.match(workflow, /"0 5 \* \* \*" # alpha/);
assert.match(workflow, /"30 5 \* \* \*" # beta/);
assert.match(workflow, /source_ref=origin\/main\^/);
assert.match(workflow, /source_ref=origin\/main\n/);
});

test("preview publication uses immutable tags and the protected release workflow", () => {
assert.match(workflow, /git push origin "refs\/tags\/\$\{tag\}"/);
assert.match(workflow, /gh workflow run release\.yml --ref "\$tag"/);
assert.match(workflow, /cp scripts\/sync-package-metadata\.js "\$RUNNER_TEMP\/sync-package-metadata\.js"/);
assert.match(workflow, /cp "\$RUNNER_TEMP\/sync-package-metadata\.js" scripts\/sync-package-metadata\.js/);
assert.doesNotMatch(workflow, /--force/);
assert.doesNotMatch(workflow, /cancel-in-progress: true/);
test("preview schedules finalize only staged signed candidates", () => {
assert.match(workflow, /"0 5 \* \* \*" # alpha/);
assert.match(workflow, /"30 5 \* \* \*" # beta/);
assert.match(workflow, /MONO_SHA256SUMS\.cosign\.bundle/);
assert.match(workflow, /select\(\.draft/);
assert.match(workflow, /contents: read/);
assert.doesNotMatch(workflow, /contents: write/);
assert.match(workflow, /gh workflow run release\.yml --ref main/);
assert.match(workflow, /No staged signed/);
assert.doesNotMatch(workflow, /git (?:push|tag|commit)/);
assert.doesNotMatch(workflow, /scripts\/version\.js/);
assert.doesNotMatch(workflow, /cancel-in-progress: true/);
});

test("channel pointers carry the signed native release contract", () => {
assert.match(releaseWorkflow, /id-token: write/);
assert.match(releaseWorkflow, /create-release-metadata\.mjs/);
assert.match(releaseWorkflow, /verify-staged-release\.mjs/);
assert.match(releaseWorkflow, /\.receipt\.sourceSha/);
assert.doesNotMatch(releaseWorkflow, /create-release-metadata\.mjs/);
assert.match(releaseWorkflow, /create-release-channel-manifest\.mjs/);
assert.match(releaseWorkflow, /softprops\/action-gh-release@[0-9a-f]{40}/);
assert.match(releaseWorkflow, /release-assets\/channel-manifest\.json/);
assert.match(releaseWorkflow, /release-assets\/manifest\.json/);
assert.match(releaseWorkflow, /release-assets\/version\.json/);
assert.match(releaseWorkflow, /cosign sign-blob --yes --bundle SHA256SUMS\.cosign\.bundle/);
assert.match(releaseWorkflow, /cosign sign-blob --yes --bundle "\$\{binary\}\.cosign\.bundle"/);
assert.match(releaseWorkflow, /files: release-assets\/\*/);
assert.match(releaseWorkflow, /files: \|\n\s+release-assets\/\*\.json/);
assert.doesNotMatch(releaseWorkflow, /\.\/\.github\/actions\/gcs-artifacts/);
assert.doesNotMatch(releaseWorkflow, /MAESTRO_RELEASES_PREFIX/);
assert.doesNotMatch(releaseWorkflow, /gcloud storage/);
assert.doesNotMatch(releaseWorkflow, /target_commitish/);
assert.doesNotMatch(releaseWorkflow, /maestro-\$\{RELEASE_CHANNEL\}-channel/);
assert.doesNotMatch(releaseWorkflow, /gh release upload "\$channel_tag"/);
});

const tagWorkflow = await readFile(new URL("./tag-release.yml", import.meta.url), "utf8");
test("tag retries dispatch main and correlate the normalized release version", () => {
assert.match(releaseWorkflow, /run-name: Release \$\{\{ startsWith/);
assert.match(tagWorkflow, /--ref main/);
assert.doesNotMatch(tagWorkflow, /--ref "\$\{RELEASE_TAG\}"/);
assert.equal((tagWorkflow.match(/\.displayTitle ==/g) || []).length, 4);
assert.equal((tagWorkflow.match(/--json [^\n]*displayTitle/g) || []).length, 4);
assert.match(tagWorkflow, /outputs\.staged_ready == 'true'/);
assert.match(tagWorkflow, /MONO_SHA256SUMS\.cosign\.bundle/);
assert.match(tagWorkflow, /elif grep -q 'HTTP 404'/);
assert.match(tagWorkflow, /cat "\$release_error" >&2\n\s+exit 1/);
});

test("only authenticated enabled device helpers are extracted and published", () => {
assert.match(releaseWorkflow, /--pattern 'code-device-\*\.json'/);
assert.match(releaseWorkflow, /verify-staged-release\.mjs[\s\S]*jq -r '\.enabled'[\s\S]*tar -xzf/);
assert.match(releaseWorkflow, /files\+=\(runtime-passport-maestro-\*\.json code-device-\*\.json\)/);
assert.doesNotMatch(releaseWorkflow, /files\+=\([^\n]*deixic-code-device-\*/);
});

test("older source verifiers cannot authorize unhashed capability markers", () => {
const authentication = releaseWorkflow.indexOf('node scripts/verify-staged-release.mjs release-binaries');
const markerCheck = releaseWorkflow.indexOf('code-device-${platform}\\.json$');
const decision = releaseWorkflow.indexOf("jq -r '.enabled'");
assert.ok(authentication >= 0 && authentication < markerCheck && markerCheck < decision);
assert.match(releaseWorkflow, /MONO_SHA256SUMS \| sha256sum --check --strict/);
});
57 changes: 16 additions & 41 deletions .github/workflows/channel-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: preview-channel-release
on:
schedule:
- cron: "0 5 * * *" # alpha
- cron: "30 5 * * *" # beta, after alpha but from one commit earlier
- cron: "30 5 * * *" # beta, after alpha
workflow_dispatch:
inputs:
channel:
Expand All @@ -23,20 +23,14 @@ concurrency:

jobs:
publish-channel-source:
if: github.repository == 'evalops/maestro' && github.ref == 'refs/heads/main'
runs-on: ${{ vars.PUBLIC_RELEASE_RUNNER || 'ubuntu-latest' }}
timeout-minutes: 30
permissions:
actions: write
contents: write
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
fetch-depth: 2
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
node-version: 24
package-manager-cache: false
- name: Create immutable preview source
- name: Select staged signed preview
id: preview
env:
GH_TOKEN: ${{ github.token }}
Expand All @@ -52,49 +46,30 @@ jobs:
*) echo "::error::Unknown channel schedule: ${SCHEDULE}"; exit 1 ;;
esac
fi

git fetch --no-tags origin main
stable_version="$(git show origin/main:package.json | jq -r .version)"
cp scripts/channel-version.mjs "$RUNNER_TEMP/channel-version.mjs"
cp scripts/sync-package-metadata.js "$RUNNER_TEMP/sync-package-metadata.js"
cp .github/workflows/release.yml "$RUNNER_TEMP/release.yml"
release="$(node "$RUNNER_TEMP/channel-version.mjs" "$stable_version" "$channel" "$GITHUB_RUN_NUMBER")"
source_offset="$(jq -r .sourceOffset <<<"$release")"
version="$(jq -r .version <<<"$release")"
source_ref=origin/main
if [[ "$source_offset" == "1" ]]; then
source_ref=origin/main^
case "$channel" in alpha|beta) ;; *) exit 1 ;; esac
# Mono owns preview source and signed builds. Finalize an existing
# staged candidate; never synthesize source that differs from its binaries.
tag="$(timeout 60s gh api "repos/${GITHUB_REPOSITORY}/releases?per_page=100" |
jq -r --arg channel "$channel" '[.[] | select(.draft and (.tag_name | test("-" + $channel + "[.-]"))) |
select(any(.assets[]; .name == "MONO_SHA256SUMS.cosign.bundle"))] |
sort_by(.created_at) | last | .tag_name // empty')"
if [[ -z "$tag" ]]; then
echo "No staged signed ${channel} release is ready." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi

git checkout --detach "$source_ref"
cp "$RUNNER_TEMP/channel-version.mjs" scripts/channel-version.mjs
cp "$RUNNER_TEMP/sync-package-metadata.js" scripts/sync-package-metadata.js
cp "$RUNNER_TEMP/release.yml" .github/workflows/release.yml
npm ci
node scripts/version.js set "$version" --release-notes-ref "$source_ref"
git config user.name "evalops-release-bot"
git config user.email "release-bot@evalops.dev"
git add -A
git commit -m "Release Maestro ${version}"
tag="v${version}"
git tag "$tag"
git push origin "refs/tags/${tag}"

gh workflow run release.yml --ref "$tag" --field "version=${version}"
gh workflow run release.yml --ref main --field "version=${tag}"
{
echo "channel=$channel"
echo "source_ref=$source_ref"
echo "tag=$tag"
} >> "$GITHUB_OUTPUT"
- name: Summarize dispatched release
if: steps.preview.outputs.tag != ''
env:
CHANNEL: ${{ steps.preview.outputs.channel }}
SOURCE_REF: ${{ steps.preview.outputs.source_ref }}
TAG: ${{ steps.preview.outputs.tag }}
run: |
{
echo "### Preview release dispatched"
echo "- Channel: ${CHANNEL}"
echo "- Product source: ${SOURCE_REF}"
echo "- Immutable tag: ${TAG}"
} >> "$GITHUB_STEP_SUMMARY"
50 changes: 41 additions & 9 deletions .github/workflows/check-release-workflow-contract.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ function indentation(line) {
}

function scalar(value) {
return value.trim().replace(/^["']|["']$/gu, "").replace(/\s+#.*$/u, "");
return value.trim().replace(/^(["'])([\s\S]*)\1$/u, "$2").replace(/\s+#.*$/u, "");
}

export function parseWorkflow(source) {
Expand Down Expand Up @@ -159,7 +159,20 @@ export function parseWorkflow(source) {
}
if (indent === 10 && (stepSection === "env" || stepSection === "with")) {
const entry = /^ ([a-zA-Z0-9_-]+):\s*(.*?)\s*$/u.exec(line);
if (entry) step[stepSection][entry[1]] = scalar(entry[2]);
if (entry) {
let value = scalar(entry[2]);
if (value === "|") {
const values = [];
while (index + 1 < lines.length) {
const next = lines[index + 1];
if (next.trim() && indentation(next) <= 10) break;
index += 1;
if (next.trim()) values.push(next.trim());
}
value = values.join("\n");
}
step[stepSection][entry[1]] = value;
}
}
}

Expand Down Expand Up @@ -249,7 +262,7 @@ export function validateReleaseWorkflow(source) {
failures.push("workflow default permissions must be exactly contents: read");
}
const normalizedReleaseConcurrency =
"${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && (startsWith(inputs.version, 'v') && inputs.version || format('v{0}', inputs.version)) || github.ref_name }}";
"${{ github.workflow }}-${{ startsWith(github.event.client_payload.version || inputs.version, 'v') && (github.event.client_payload.version || inputs.version) || format('v{0}', github.event.client_payload.version || inputs.version) }}";
if (concurrencyGroup !== normalizedReleaseConcurrency) {
failures.push(
"release workflows must serialize only duplicate paths for the same normalized release tag",
Expand Down Expand Up @@ -292,7 +305,9 @@ export function validateReleaseWorkflow(source) {
["post-publish-canary", canary],
]) {
if (
job.condition ||
(name === "prepare"
? job.condition !== "github.repository == 'evalops/maestro' && github.ref == 'refs/heads/main'"
: job.condition) ||
(job.continueOnError && job.continueOnError !== "false")
) {
failures.push(`${name} job must not be conditional or ignored`);
Expand All @@ -315,9 +330,9 @@ export function validateReleaseWorkflow(source) {
const prepareCheckout = prepare.steps.find((step) =>
step.uses.startsWith("actions/checkout@"),
);
if (prepareCheckout?.with["fetch-depth"] !== "1") {
if (prepareCheckout?.with["fetch-depth"] !== "0") {
failures.push(
"prepare checkout must be shallow before the bounded immutable tag fetch",
"prepare checkout must include history for protected main ancestry verification",
);
}
const resolveLines = executableLines(resolveStep?.run ?? "");
Expand All @@ -343,15 +358,15 @@ export function validateReleaseWorkflow(source) {
if (
!hasExactRecord(resolveStep?.env ?? {}, {
EVENT_NAME: "${{ github.event_name }}",
REQUESTED: "${{ github.event.inputs.version || github.ref_name }}",
REQUESTED: "${{ github.event.client_payload.version || github.event.inputs.version || github.ref_name }}",
TRIGGER_SHA: "${{ github.sha }}",
})
) {
failures.push("immutable resolver must bind the triggering event and SHA");
}
const triggerShaIndex = resolveLines.indexOf('release_sha="$TRIGGER_SHA"');
const dispatchIndex = resolveLines.indexOf(
'if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then',
'if [[ "$EVENT_NAME" == "workflow_dispatch" || "$EVENT_NAME" == "repository_dispatch" ]]; then',
);
const fetchedShaIndex = resolveLines.indexOf(
'release_sha="$(git rev-list -n 1 "$release_tag")"',
Expand All @@ -377,6 +392,12 @@ export function validateReleaseWorkflow(source) {
if (!topLevelResolveLines.includes('git checkout --detach "$release_sha"')) {
failures.push("immutable release resolution must check out the resolved SHA");
}
const ancestryIndex = topLevelResolveLines.indexOf('git merge-base --is-ancestor "$release_sha" FETCH_HEAD');
if (ancestryIndex < 0 || topLevelResolveLines.indexOf('timeout 60s git fetch --no-tags origin main') >= ancestryIndex ||
!topLevelResolveLines.includes('timeout 60s git fetch --no-tags origin main') ||
ancestryIndex >= topLevelResolveLines.indexOf('git checkout --detach "$release_sha"')) {
failures.push("release source must pass protected main ancestry verification before checkout");
}
const boundedFetch =
'fetch --force --no-tags origin "refs/tags/${release_tag}:refs/tags/${release_tag}"; then';
if (
Expand Down Expand Up @@ -413,6 +434,17 @@ export function validateReleaseWorkflow(source) {
failures.push("immutable resolver outputs must use one grouped GITHUB_OUTPUT write");
}

const authenticate = findStep(binaries, "Authenticate artifacts and release receipts");
const authLines = executableLines(authenticate?.run ?? "");
// A strict shell preamble is permitted; no other command may precede authentication.
if (authLines[0] === "set -euo pipefail") authLines.shift();
const verifyCommand = 'node scripts/verify-staged-release.mjs release-binaries "$RELEASE_VERSION"';
const authIndex = binaries.steps.indexOf(authenticate);
if (requiredStepCanBeSkippedOrIgnored(authenticate) || authLines[0] !== verifyCommand ||
authenticate?.env.RELEASE_VERSION !== "${{ needs.prepare.outputs.release_version }}" ||
binaries.steps.some((step, index) => index < authIndex && (step.run.includes("tar -x") || step.uses.startsWith("actions/upload-artifact@")))) {
failures.push("native artifacts must be authenticated for the immutable version before extraction or upload");
}
if (!hasNeed(binaries, "prepare")) failures.push("binaries must need prepare");
if (!hasNeed(publish, "prepare") || !hasNeed(publish, "binaries")) {
failures.push("publish must need prepare and binaries");
Expand Down Expand Up @@ -662,7 +694,7 @@ export function validateReleaseWorkflow(source) {
"${{ needs.prepare.outputs.release_tag }}" ||
releaseStep?.with.name !==
"Maestro ${{ needs.prepare.outputs.release_version }}" ||
releaseStep?.with.files !== "release-assets/*"
releaseStep?.with.files !== "release-assets/*.json\nrelease-assets/*.tgz\nrelease-assets/*.tar.gz\nrelease-assets/*.txt\nrelease-assets/*SUMS\nrelease-assets/*.bundle\nrelease-assets/maestro-linux-*\nrelease-assets/maestro-darwin-*"
) {
failures.push(
"GitHub release metadata and files must bind to immutable prepare outputs",
Expand Down
Loading
Loading