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
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ jobs:
- name: Test public release metadata and documentation
run: npm run smoke:public-release

- name: Test release changelog and curated notes
run: npm run smoke:release-notes

- name: Test published release closure verifier
run: npm run smoke:published-release

- name: Test release metadata contract
run: npm run smoke:release-metadata

Expand Down
76 changes: 70 additions & 6 deletions .github/workflows/release-unsigned.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ on:
required: false
default: ''
type: string
creator_qualification:
description: "Human declaration required for publication: NOT_REQUIRED or PASSED_PHYSICAL_MAC"
required: false
default: ''
type: string
qualification_reference:
description: Durable issue, PR/comment URL, or test record for PASSED_PHYSICAL_MAC
required: false
default: ''
type: string

permissions: {}

Expand Down Expand Up @@ -66,12 +76,17 @@ jobs:
PUBLISH: ${{ inputs.publish }}
REAL_MODEL: ${{ inputs.real_model }}
CONFIRMATION: ${{ inputs.confirmation }}
CREATOR_QUALIFICATION: ${{ inputs.creator_qualification }}
QUALIFICATION_REFERENCE: ${{ inputs.qualification_reference }}
run: |
test "$(uname -m)" = "arm64"
test "$(node -p process.arch)" = "arm64"
git fetch --tags --force origin
git tag --list > "$RUNNER_TEMP/scriptcut-existing-tags.txt"
node scripts/prepare-public-release.js --validate-tag --tag "$RELEASE_TAG" --existing-tags-file "$RUNNER_TEMP/scriptcut-existing-tags.txt"
node scripts/check-published-release.js --validate-qualification \
--creator-qualification "$CREATOR_QUALIFICATION" \
--qualification-reference "$QUALIFICATION_REFERENCE"
if gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
echo "Release already exists: $RELEASE_TAG"
exit 1
Expand All @@ -80,6 +95,10 @@ jobs:
test "$GITHUB_REF" = "refs/heads/main"
test "$REAL_MODEL" = "true"
test "$CONFIRMATION" = "PUBLISH_UNSIGNED_ALPHA"
test "$CREATOR_QUALIFICATION" = "NOT_REQUIRED" || test "$CREATOR_QUALIFICATION" = "PASSED_PHYSICAL_MAC"
if [ "$CREATOR_QUALIFICATION" = "PASSED_PHYSICAL_MAC" ]; then
test -n "$QUALIFICATION_REFERENCE"
fi
fi

- name: Build ad-hoc self-contained candidate
Expand Down Expand Up @@ -118,12 +137,18 @@ jobs:
- name: Prepare public release bundle
env:
RELEASE_TAG: ${{ inputs.release_tag }}
PUBLISH: ${{ inputs.publish }}
run: |
NOTE_ARGS=()
if [ "$PUBLISH" = "true" ]; then
NOTE_ARGS+=(--require-release-notes)
fi
node scripts/prepare-public-release.js \
--tag "$RELEASE_TAG" \
--candidate-dir dist/release-candidate \
--output-dir dist/public-release \
--commit "$GITHUB_SHA"
--commit "$GITHUB_SHA" \
"${NOTE_ARGS[@]}"

- name: Check public bundle before attestation
run: |
Expand Down Expand Up @@ -325,10 +350,19 @@ jobs:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ inputs.release_tag }}
CONFIRMATION: ${{ inputs.confirmation }}
CREATOR_QUALIFICATION: ${{ inputs.creator_qualification }}
QUALIFICATION_REFERENCE: ${{ inputs.qualification_reference }}
run: |
test "$GITHUB_REF" = "refs/heads/main"
test "$CONFIRMATION" = "PUBLISH_UNSIGNED_ALPHA"
test "${{ inputs.real_model }}" = "true"
test "$CREATOR_QUALIFICATION" = "NOT_REQUIRED" || test "$CREATOR_QUALIFICATION" = "PASSED_PHYSICAL_MAC"
if [ "$CREATOR_QUALIFICATION" = "PASSED_PHYSICAL_MAC" ]; then
test -n "$QUALIFICATION_REFERENCE"
fi
node scripts/check-published-release.js --validate-qualification \
--creator-qualification "$CREATOR_QUALIFICATION" \
--qualification-reference "$QUALIFICATION_REFERENCE"
git fetch origin main --tags --force
test "$(git rev-parse origin/main)" = "$GITHUB_SHA"
git tag --list > "$RUNNER_TEMP/scriptcut-existing-tags.txt"
Expand Down Expand Up @@ -365,12 +399,42 @@ jobs:
dist/public-release/ScriptCut-${RELEASE_TAG}-arm64.dmg.sigstore.json \
dist/public-release/release-manifest.sigstore.json

- name: Verify published tag, prerelease state, assets, and digest
- name: Verify published release and write closure evidence
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ inputs.release_tag }}
CREATOR_QUALIFICATION: ${{ inputs.creator_qualification }}
QUALIFICATION_REFERENCE: ${{ inputs.qualification_reference }}
run: |
git fetch origin "refs/tags/$RELEASE_TAG"
test "$(git ls-remote origin "refs/tags/$RELEASE_TAG" "refs/tags/$RELEASE_TAG^{}" | tail -n 1 | awk '{print $1}')" = "$GITHUB_SHA"
gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --json isDraft,isPrerelease,tagName,assets > "$RUNNER_TEMP/published-release.json"
node scripts/check-public-release.js --release-json "$RUNNER_TEMP/published-release.json" --dir dist/public-release
QUALIFICATION_ARGS=(--creator-qualification "$CREATOR_QUALIFICATION")
if [ -n "$QUALIFICATION_REFERENCE" ]; then
QUALIFICATION_ARGS+=(--qualification-reference "$QUALIFICATION_REFERENCE")
fi
node scripts/check-published-release.js \
--repo "$GITHUB_REPOSITORY" \
--tag "$RELEASE_TAG" \
--commit "$GITHUB_SHA" \
--dir dist/public-release \
--output "$RUNNER_TEMP/release-closure.json" \
--workflow "$GITHUB_WORKFLOW" \
--run-id "$GITHUB_RUN_ID" \
--run-attempt "$GITHUB_RUN_ATTEMPT" \
"${QUALIFICATION_ARGS[@]}"
{
echo "## Published release closure"
echo "- releaseTag: $RELEASE_TAG"
echo "- commit: $GITHUB_SHA"
echo "- DMG SHA-256: $(node -p "require('./dist/public-release/release-manifest.json').artifact.sha256")"
echo "- creator qualification: $CREATOR_QUALIFICATION"
if [ -n "$QUALIFICATION_REFERENCE" ]; then echo "- qualification reference: $QUALIFICATION_REFERENCE"; fi
echo "- release URL: $(node -e "const fs=require('fs'); const p=JSON.parse(fs.readFileSync('$RUNNER_TEMP/release-closure.json','utf8')); console.log(p.release.url)")"
echo "- post-publish verification: PASS"
} >> "$GITHUB_STEP_SUMMARY"

- name: Upload release closure evidence
uses: actions/upload-artifact@v4
with:
name: scriptcut-${{ inputs.release_tag }}-release-closure-evidence
if-no-files-found: error
retention-days: 30
path: ${{ runner.temp }}/release-closure.json
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Changelog

## Unreleased

### Added

- Canonical release-note and post-publication closure verification for the public alpha workflow.

### Changed

- Release qualification now separates product identity, public release identity, and maintainer evidence.
- Public release preparation can use curated, creator-oriented changes without duplicating them in generator code.

### Fixed

- Post-publication release verification now has an explicit reusable check of the live GitHub release state.

## v0.1.0-alpha.3

- First qualified self-contained public macOS Apple Silicon alpha with local baseline Whisper transcription, bundled runtime and FFmpeg, and verifiable GitHub release provenance.
39 changes: 38 additions & 1 deletion docs/RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,40 @@ Use `npm run dist:dir` when you only need an unpacked app bundle for local QA.

The dedicated `.github/workflows/release-unsigned.yml` workflow is `workflow_dispatch` only. It builds on a native `macos-14` arm64 runner using the existing `npm run release:rc:arm64` candidate machinery, then stages the exact public DMG, public manifest, notes, checksum, and Sigstore attestation bundles. The resulting app uses an ad-hoc code signature for package integrity; the workflow name and input identifiers retain their existing compatibility names.

Required inputs are `release_tag`, `publish`, `real_model`, and `confirmation`. The tag must be `v<package.version>-alpha.<positive integer>` and must be greater than every existing alpha tag. `publish=false` is the safe dry-run mode: it may create attestations and a workflow artifact whose name includes `dry-run`, but it cannot create a tag, release, commit, or mutate `main`. Publication additionally requires `publish=true`, `real_model=true`, `confirmation=PUBLISH_UNSIGNED_ALPHA`, the workflow ref to be `main`, and a current `origin/main` equal to the dispatched commit. The publish job has contents write permission only, downloads the already verified artifact, creates a GitHub prerelease with `--prerelease --latest=false`, and verifies the exact tag, asset set, and digest after creation. It never rebuilds during publication.
Required inputs are `release_tag`, `publish`, `real_model`, and `confirmation`. Publication also requires the explicit human `creator_qualification` declaration `NOT_REQUIRED` or `PASSED_PHYSICAL_MAC`; the latter requires a non-empty `qualification_reference`. `NOT_REQUIRED` is valid only when [Release QA](./RELEASE_QA.md) says the physical creator gate is unnecessary. The declaration is not an automated physical test result. The tag must be `v<package.version>-alpha.<positive integer>` and must be greater than every existing alpha tag. `publish=false` is the safe dry-run mode: it may create attestations and a workflow artifact whose name includes `dry-run`, but it cannot create a tag, release, commit, or mutate `main`. Publication additionally requires `publish=true`, `real_model=true`, `confirmation=PUBLISH_UNSIGNED_ALPHA`, the workflow ref to be `main`, and a current `origin/main` equal to the dispatched commit. The publish job has contents write permission only, downloads the already verified artifact, creates a GitHub prerelease with `--prerelease --latest=false`, and runs the reusable `scripts/check-published-release.js` verifier after creation. It never rebuilds during publication.

## Changelog and release lifecycle

`CHANGELOG.md` is the single concise, human-maintained source for meaningful
creator-facing changes. Keep current work under `Unreleased`; do not record
every commit or internal refactor. Before publication, curate the relevant
entries into an exact `## v<package.version>-alpha.<n>` section in a normal
commit or PR. The public `RELEASE_NOTES.md` generator adds that section under
`What's changed` while retaining the authoritative technical sections.

The durable lifecycle is:

```text
Maintain CHANGELOG Unreleased
→ prepare an exact releaseTag section
→ merge to main
→ run the public dry-run
→ complete manual creator qualification when required
→ publish with an explicit qualification declaration
→ verify live GitHub release state
→ retain closure evidence in Actions
```

Dry-runs may render `Unreleased` when an exact release section is not present,
and label that content as planned. Publication refuses to proceed without the
exact release section. The workflow never edits `CHANGELOG.md` or commits back
to `main`.

`release-manifest.json` remains artifact/build provenance. The GitHub tag,
Release, and six public assets remain the published release authority.
`scripts/check-published-release.js` verifies that live state, while
`release-closure.json` records one successful verification and is retained as
workflow evidence only; it is not a public release asset.

The closure procedure is a `publish=false` dry-run from current `main`. Run it
before any separately authorized public publication; it creates no tag or
Expand Down Expand Up @@ -158,6 +191,10 @@ Attach:
- `dist/public-release/ScriptCut-v<version>-alpha.<n>-arm64.dmg.sigstore.json`
- `dist/public-release/release-manifest.sigstore.json`

The public release remains exactly these six assets. Closure evidence is
uploaded to GitHub Actions under a separate evidence artifact with operational
retention; it is not added to the GitHub Release.

The public release manifest uses schema `scriptcut.release.v2` and records the ad-hoc structural signature, Apple Developer ID, and notarization truth, bundled runtime/core/FFmpeg/model provenance, final DMG SHA-256, release tag, source commit, and the DMG attestation reference. The DMG and manifest are each attested with the official GitHub artifact-attestation action; the workflow artifact bundles are retained for independent verification.

## Signing And Notarization
Expand Down
36 changes: 36 additions & 0 deletions docs/RELEASE_QA.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,27 @@ The identity contract is intentionally unchanged:
- A developer build, candidate, dry-run, qualified public artifact, and
published release are different states.

## Durable release lifecycle

```text
Maintain CHANGELOG Unreleased
→ prepare an exact release-specific changelog section
→ merge to main
→ run the public dry-run
→ complete manual creator qualification when required
→ publish
→ verify live GitHub release state
→ retain closure evidence
```

`CHANGELOG.md` is intentionally lightweight: record meaningful creator-facing
changes, compatibility, security, installation, or release-integrity changes;
do not turn it into an exhaustive commit log. Before publication, curate the
relevant `Unreleased` entries into the exact `releaseTag` section. Public notes
are generated from that source. Dry-runs may use `Unreleased` when the exact
section is not yet present, but publication requires the exact non-empty
section and never mutates the changelog.

## Gate matrix

| Gate | Owner stage | Runner | What it proves | Required | Conditional | Network / model download | Can mutate GitHub |
Expand Down Expand Up @@ -69,6 +90,14 @@ first-use flow, video open/preview, transcription, or the core export path.
Release-system-only changes that do not alter the public artifact or creator
runtime may mark creator qualification as not required.

At publication time the maintainer must explicitly declare either
`NOT_REQUIRED` or `PASSED_PHYSICAL_MAC`. `NOT_REQUIRED` is valid only when the
policy above says the physical creator gate is unnecessary.
`PASSED_PHYSICAL_MAC` is a human declaration, not an automated test result,
and requires a durable `qualification_reference` such as an issue, PR/comment
URL, or concise test record identifier. The workflow does not infer this value
from changed filenames.

Physical MPS validation is required only when the change touches Torch/MPS
device behavior, Whisper MPS timing compatibility, GPU execution selection,
MPS-specific transcription code, or native GPU dependency/runtime behavior.
Expand Down Expand Up @@ -102,6 +131,13 @@ There is no public npm publisher. Publication remains exclusively owned by
candidate bytes, attests them, verifies them on a clean native runner, and
publishes only the already verified artifact.

After the release is created, `scripts/check-published-release.js` verifies the
live tag, release, exact six-asset set, server-side asset digests, and exact
release body. The successful publish job writes `release-closure.json` and a
summary containing the release identity, DMG digest, qualification declaration,
and release URL. The closure file is operational workflow evidence, not a
second manifest, trust document, proof of human qualification, or public asset.

## Trust and mutation invariants

The public path remains current-main-only and requires
Expand Down
19 changes: 19 additions & 0 deletions docs/VERIFY_RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,22 @@ Replace the example DMG name and `<commit-sha>` with the values in `release-mani
## First launch

Because the public alpha is ad-hoc-signed but not signed with Apple Developer ID or notarized, macOS may block it. For a DMG downloaded from the official Releases feed, use **System Settings → Privacy & Security → Open Anyway** and confirm the prompt. Do not disable Gatekeeper or remove quarantine attributes.

## Maintainer publication closure

Creator verification above establishes artifact integrity and build provenance
from the files in the public release. Maintainers use the separate
`scripts/check-published-release.js` tool after publication to compare live
GitHub state with the already verified local six-file bundle:

```bash
node scripts/check-published-release.js \
--repo FernandoAbishai/ScriptCut \
--tag v0.1.0-alpha.3 \
--commit <commit-sha> \
--dir dist/public-release \
--output release-closure.json
```

This is post-publication closure evidence, not an additional creator
installation step and not a replacement for `release-manifest.json`.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
"smoke:packaged-transcription": "node scripts/smoke-packaged-transcription.js --arch arm64",
"smoke:packaged-optional-capabilities": "node scripts/smoke-packaged-optional-capabilities.js --arch arm64",
"smoke:release-metadata": "node scripts/smoke-release-metadata.js",
"smoke:release-notes": "node scripts/smoke-release-notes.js",
"smoke:published-release": "node scripts/smoke-published-release.js",
"smoke:release-identity": "node scripts/smoke-release-identity.js",
"release:public:prepare": "node scripts/prepare-public-release.js",
"check:public-release": "node scripts/check-public-release.js",
Expand Down
Loading
Loading