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
2 changes: 1 addition & 1 deletion .claude/policy-brief.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<!-- GENERATED FILE: edit agent-policy.json and run scripts/agent/generate-adapters.py --write -->
# Loop agent policy adapter

Repository: `studio-berry/loop`; version: `0.2.0-alpha`; language: `C++20`; minimum Qt: `6.11.1`.
Repository: `studio-berry/loop`; version: `0.2.1-alpha`; language: `C++20`; minimum Qt: `6.11.1`.

## Branches and safety

Expand Down
2 changes: 1 addition & 1 deletion .cursor/agent-policy.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<!-- GENERATED FILE: edit agent-policy.json and run scripts/agent/generate-adapters.py --write -->
# Loop agent policy adapter

Repository: `studio-berry/loop`; version: `0.2.0-alpha`; language: `C++20`; minimum Qt: `6.11.1`.
Repository: `studio-berry/loop`; version: `0.2.1-alpha`; language: `C++20`; minimum Qt: `6.11.1`.

## Branches and safety

Expand Down
4 changes: 4 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

<!-- One paragraph: the problem, the change, and the issue. `dev` is not the default branch, so close the issue explicitly with `gh issue close`. -->

## Release changelog

<!-- Promotion and release PRs (`dev` -> `unstable` -> `stable`) only: the full changelog goes here, shaped by `docs/CHANGELOG_FORMAT.md` - measured scope line, Added/Changed/Fixed/Security/Internal, one `Closes #<n>` line per issue whose acceptance criteria the release meets, `Refs #<n>` plus the outstanding criterion otherwise, verification naming workflows and SHAs, and a stated breaking-change answer. Topic PRs answer with their `changes/<branch>.md` fragment and leave this comment in place. -->

## Proof

- [ ] `python scripts/agent/check-change.py --base origin/dev --build-dir build-local` reports `pass`, or the report and the failing check are quoted here
Expand Down
113 changes: 67 additions & 46 deletions .github/workflows/CreateReleaseDraft.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
name: Create_Release_Draft
run-name: Create_Release_Draft (${{ inputs.source_sha }})

on:
workflow_dispatch:
inputs:
allow_existing_release:
description: 'Allow uploading assets to an existing release'
description: 'Allow adding assets to an existing draft at the same source SHA'
required: true
type: boolean
default: false
Expand All @@ -13,14 +14,19 @@ on:
required: true
type: string

# Least privilege for GITHUB_TOKEN: checkout reads the repo; release creation and
# all gh calls use the MY_GITHUB_TOKEN PAT explicitly.
permissions:
contents: read
contents: write
actions: read

concurrency:
group: release-draft-${{ inputs.source_sha }}
cancel-in-progress: false

jobs:
build:
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ github.token }}

steps:
- name: Checkout repository
Expand All @@ -42,38 +48,54 @@ jobs:
echo "::error::Checkout SHA $actual_sha does not match source_sha $EXPECTED_SOURCE_SHA"
exit 1
fi
echo "LOOP_SOURCE_SHA=$actual_sha" >> "$GITHUB_ENV"

- name: Install tools
run: |
sudo apt-get install gh
sudo apt-get install jq
- name: Verify version policy
run: python3 scripts/ci/check_version_policy.py

- name: Read version
id: get_version
run: |
version=$(grep -oP 'set\(LOOP_VERSION \K[0-9]+\.[0-9]+\.[0-9]+' "CMakeLists.txt")
prerelease=$(grep -oP 'set\(LOOP_VERSION_PRERELEASE \K[A-Za-z0-9.-]+' "CMakeLists.txt" || true)
echo "loop_prerelease=false" >> "$GITHUB_ENV"
if [ -n "$prerelease" ]; then
version="${version}-${prerelease}"
echo "loop_prerelease=true" >> "$GITHUB_ENV"
fi
echo "Version: $version"
echo "loop_version=$version" >> $GITHUB_ENV

- name: Stop if release already exists
- name: Check release and tag identity
env:
LOOP_VERSION: ${{ env.loop_version }}
GITHUB_REPOSITORY: ${{ github.repository }}
ALLOW_EXISTING_RELEASE: ${{ inputs.allow_existing_release }}
GH_TOKEN: ${{ secrets.MY_GITHUB_TOKEN }}
run: |
set -euo pipefail
tag="v${LOOP_VERSION}"
if gh release view "$tag" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then
if [ "${ALLOW_EXISTING_RELEASE}" != "true" ]; then
echo "::error::Release $tag already exists. Refusing to upload assets because this would modify an existing release."
echo "::error::Rerun this workflow with allow_existing_release=true only if updating that release is intentional."
if git show-ref --verify --quiet "refs/tags/$tag"; then
tag_sha=$(git rev-parse "refs/tags/$tag^{commit}")
if [ "$tag_sha" != "$LOOP_SOURCE_SHA" ]; then
echo "::error::Tag $tag points to $tag_sha, not $LOOP_SOURCE_SHA."
exit 1
fi
fi
release=$(gh api --paginate "repos/${GITHUB_REPOSITORY}/releases" \
--jq ".[] | select(.tag_name == \"$tag\")")
if [ -n "$release" ]; then
if [ "$(jq -r .draft <<< "$release")" != "true" ]; then
echo "::error::Release $tag is published. Only drafts may be updated."
exit 1
fi
release_target=$(jq -r .target_commitish <<< "$release")
if [ "$release_target" != "$LOOP_SOURCE_SHA" ]; then
echo "::error::Draft $tag targets $release_target, not $LOOP_SOURCE_SHA."
exit 1
fi
if [ "$ALLOW_EXISTING_RELEASE" != "true" ]; then
echo "::error::Draft $tag exists. Set allow_existing_release=true to add missing assets."
exit 1
fi
echo "Release $tag already exists and allow_existing_release=true, continuing."
else
echo "Release $tag does not exist, continuing."
fi
Expand All @@ -97,8 +119,7 @@ jobs:
fi
echo "linux_run_id=$(echo "$latest_run" | jq -r .databaseId)" >> $GITHUB_ENV
env:
GH_TOKEN: ${{ secrets.MY_GITHUB_TOKEN }}
EXPECTED_SOURCE_SHA: ${{ inputs.source_sha }}
EXPECTED_SOURCE_SHA: ${{ env.LOOP_SOURCE_SHA }}

- name: Get latest run ID for Windows_MSI
id: get_windows_run_id
Expand All @@ -115,31 +136,28 @@ jobs:
fi
echo "windows_run_id=$(echo "$latest_run" | jq -r .databaseId)" >> $GITHUB_ENV
env:
GH_TOKEN: ${{ secrets.MY_GITHUB_TOKEN }}
EXPECTED_SOURCE_SHA: ${{ inputs.source_sha }}
EXPECTED_SOURCE_SHA: ${{ env.LOOP_SOURCE_SHA }}

- name: Download Linux_AppImage artifact
run: |
mkdir -p artifacts
echo "Downloading artifacts from run ID: ${{ env.linux_run_id }}..."
gh run download ${{ env.linux_run_id }} -D artifacts
gh run download "$linux_run_id" --pattern "Loop-pdf-${loop_version}-x86_64.AppImage*" -D artifacts
echo "Artifacts downloaded to artifacts directory."
env:
GH_TOKEN: ${{ secrets.MY_GITHUB_TOKEN }}

- name: Download Windows_MSI artifact
run: |
mkdir -p artifacts
echo "Downloading artifacts from run ID: ${{ env.windows_run_id }}..."
gh run download ${{ env.windows_run_id }} -D artifacts
gh run download "$windows_run_id" \
--name "Loop-pdf-Windows-${loop_version}.zip" \
--name "mberrys.Loop-pdf_${loop_version}.msi" \
--name "mberrys.Loop-pdf_${loop_version}.msix" -D artifacts
echo "Artifacts downloaded to artifacts directory."
env:
GH_TOKEN: ${{ secrets.MY_GITHUB_TOKEN }}

- name: Download and validate package boundary evidence
env:
GH_TOKEN: ${{ secrets.MY_GITHUB_TOKEN }}
EXPECTED_SOURCE_SHA: ${{ inputs.source_sha }}
EXPECTED_SOURCE_SHA: ${{ env.LOOP_SOURCE_SHA }}
run: |
set -euo pipefail
mkdir -p package-evidence/linux package-evidence/windows
Expand All @@ -161,12 +179,12 @@ jobs:
retention-days: 30
compression-level: 0

- name: Exclude CI evidence from release assets
- name: Stage final-artifact SBOMs and notices
run: |
for evidence_dir in artifacts/loop-package-boundary-linux-evidence artifacts/loop-package-boundary-windows-evidence; do
if [ -e "$evidence_dir" ]; then
rm -rf "$evidence_dir"
fi
set -euo pipefail
for platform in linux windows; do
cp "package-evidence/$platform/components.spdx.json" "artifacts/Loop-${loop_version}-${platform}.spdx.json"
cp "package-evidence/$platform/THIRD_PARTY_NOTICES.txt" "artifacts/Loop-${loop_version}-${platform}-THIRD_PARTY_NOTICES.txt"
done

- name: Rezip zip file
Expand All @@ -183,30 +201,32 @@ jobs:
- name: Correct file paths
run: |
set -euo pipefail
# Flatten artifact subdirectories. Fail loudly on a name collision:
# silently overwriting means a release asset is dropped, and
# fail_on_unmatched_files cannot detect that because the glob still matches.
# Artifact directories can share the payload filename.
mkdir -p release-assets
while IFS= read -r -d '' src; do
dest="artifacts/$(basename "$src")"
dest="release-assets/$(basename "$src")"
if [ -e "$dest" ]; then
echo "::error::Artifact file name collision while flattening: $src -> $dest"
exit 1
fi
mv "$src" "$dest"
done < <(find artifacts -mindepth 2 -type f -print0)
done < <(find artifacts -type f -print0)
find artifacts -type d -empty -delete

- name: List all files recursively
- name: Verify release assets against inspected packages
run: |
echo "Listing all files in artifacts:"
find artifacts -type f -print
python3 scripts/ci/verify_release_assets.py \
--artifacts release-assets \
--pair package-evidence/pair.json \
--version "$loop_version" \
--source-sha "$LOOP_SOURCE_SHA"

- name: Generate SHA-256 checksums
run: |
# docs/PACKAGING_LICENSING.md: "Sign artifacts and publish checksums."
# Checksums are published here; Authenticode signing of the MSI is gated on
# vars.SIGN_MSI in WindowsInstall.yml and requires the code-signing cert.
cd artifacts
cd release-assets
# Exclude the output file: the redirection creates it before find runs, so an
# unfiltered glob hashes an empty SHA256SUMS.txt into its own listing.
find . -maxdepth 1 -type f ! -name SHA256SUMS.txt -printf '%P\0' \
Expand All @@ -219,12 +239,13 @@ jobs:
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2
with:
tag_name: 'v${{ env.loop_version }}'
name: "Release of Loop-pdf ${{ env.loop_version }} (Draft)"
name: "Loop ${{ env.loop_version }}"
target_commitish: ${{ env.LOOP_SOURCE_SHA }}
draft: true
prerelease: false
prerelease: ${{ env.loop_prerelease == 'true' }}
generate_release_notes: true
token: ${{ secrets.MY_GITHUB_TOKEN }}
token: ${{ github.token }}
fail_on_unmatched_files: true
overwrite_files: false
files: |
artifacts/*
release-assets/*
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Test release assets against paired package evidence
run: python -m unittest scripts.ci.test_verify_release_assets -v
- name: Test package lifecycle scripts with fake packages
run: python -m unittest scripts.ci.test_run_qt_relink_test -v
- name: Test Linux AppImage Qt relink script with fake AppImage
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<!-- GENERATED FILE: edit agent-policy.json and run scripts/agent/generate-adapters.py --write -->
# Loop agent policy adapter

Repository: `studio-berry/loop`; version: `0.2.0-alpha`; language: `C++20`; minimum Qt: `6.11.1`.
Repository: `studio-berry/loop`; version: `0.2.1-alpha`; language: `C++20`; minimum Qt: `6.11.1`.

## Branches and safety

Expand Down
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

cmake_minimum_required(VERSION 3.16)

set(LOOP_VERSION 0.2.0)
set(LOOP_VERSION 0.2.1)
set(LOOP_VERSION_PRERELEASE alpha)
set(LOOP_PRODUCT_NAME "Loop")
set(LOOP_ORGANIZATION_NAME "Loop")
Expand Down
4 changes: 4 additions & 0 deletions changes/fix-release-0.2.1-alpha.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Category: internal
Audience: contributors
Breaking-Change: no
Summary: Prepare 0.2.1-alpha and repair draft-release authentication, prerelease and source identity, package digest verification, and SBOM/notice attachment.
89 changes: 89 additions & 0 deletions docs/CHANGELOG_FORMAT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Loop release changelog format

The canonical shape for every Loop changelog read outside the diff: promotion PR bodies
(`dev` → `unstable` → `stable`), GitHub release notes and release drafts, and milestone or
version closeouts. It does not replace the per-PR fragment
`changes/<sanitized-head-branch>.md` (Category, Audience, Breaking-Change, Summary), which
every PR still adds exactly once.

## Skeleton

```markdown
# [<version>] <milestone canonical name>

<one paragraph: what the release is, which milestone it closes, what it rules out>

Scope: `<base tag>` (`<base sha>`) → `<tip sha>` — <n> non-merge commits.
Canonical milestone text: `docs/github-milestones/<version>.md`.

## Added

## Changed

## Fixed

## Security

## Internal

## Closes

Closes #<n>

## Verification

## Breaking changes
```

## Rules

1. **Title** is `[<version>] <milestone canonical name>`, copied verbatim from
`docs/github-milestones/<version>.md`. Work outside a milestone names its train instead of
inventing a title.
2. **The scope line is measured, not estimated.** Take the count from
`git log --oneline --no-merges <base>..<tip> | wc -l` and the base from `git describe --tags`
or the release tag, and name the range by tag **and** SHA.
3. **Sections use the fragment vocabulary in this order:** Added, Changed, Fixed, Security,
Internal. Drop what is empty; never introduce a section the vocabulary does not have.
4. **Every item names its issue and its mechanism** — `(#<n>)` plus what the code now does, or
the file or API that carries it. Adjectives do not replace behaviour, and a catch-all
"various fixes" is not an item.
- Good: `#520 — searchDocumentText() allocates an operation-scoped PDFProcessingBudget, so an idle session's elapsed timer no longer throws through Quick search.`
- Bad: `Improved search reliability.`
5. **Group by outcome, not by commit.** A twelve-commit hardening train is one item naming the
class of input and the invariant it now enforces.
6. **`Closes` is a claim about acceptance criteria, not about commits.** An issue is closed only
when the shipped diff meets the criteria written in its body. Partial work goes to `Refs`
with the outstanding criterion named. A closing keyword is never added to make a milestone
read as finished.
7. **One keyword per line.** `Closes #10` and `Closes #11` are two lines; a comma list is not
this format.
8. **Record who closes them.** Closing keywords act only when the PR merges into the default
branch (`stable`). Work that landed on `dev` or `unstable` leaves its issue open by design,
and the block says so rather than leaving the reader to work it out.
9. **Verification cites observable state** — workflow, SHA or tag, and result. "Verified
locally" without the command and its output is not verification.
10. **Breaking changes are stated, never implied.** Either `None.` with the reason it is none
(additive API, no schema or persistence-format bump), or the migration and its ordering
constraint.
11. **No claim a `git` or `gh` command cannot back.** Counts, PR numbers, SHAs, tags, check
conclusions and issue states are copied from command output. When a fact could not be
established, the changelog says that instead of asserting it.

## Placement

- **Promotion PRs** carry the whole format in the PR body. The PR body is the changelog.
- **Releases** reuse that body for the release notes or the generated release draft; a second
copy is not maintained by hand.
- **Milestones** may copy the merged promotion body into their evidence directory. The PR body
stays authoritative until the copy is made.

## Worked example

`[0.2.1] Operator Completion, Product Surface & Trust Leftovers`, the promotion
`unstable` → `stable` for milestone 17: a scope line over `0.2.0.1-alpha` (`86ee1b5b`) →
`d2e9ce9f`; items under Added / Changed / Fixed / Security / Internal, each naming its issue and
mechanism; sixteen `Closes` lines with `#236` marked as already shipped in `0.2.0.1-alpha`;
`Refs #241` with the outstanding criterion named (the renderer-differential lane and documented
corpus licensing); a verification line naming four green workflows at a SHA; and
`Breaking changes: None.` with the reason.
Loading
Loading