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
20 changes: 20 additions & 0 deletions .github/workflows/build-and-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,25 @@ on:
type: string

jobs:
validate-changelog:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Check CHANGELOG entry exists for this release
run: |
RAW_TAG="${{ github.event.release.tag_name || github.event.inputs.tag }}"
VERSION="${RAW_TAG#v}"
if ! grep -q "## \[${VERSION}\]" CHANGELOG.md; then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use grep -F for literal string matching to prevent regex injection.

The grep pattern on line 25 treats VERSION as a regex pattern. If a release tag contains regex metacharacters (e.g., v1.2.0.*, which Git allows), the pattern could match unintended CHANGELOG entries, bypassing the validation. Use the -F flag to match the version string literally.

🔒 Proposed fix for literal string matching
- if ! grep -q "## \[${VERSION}\]" CHANGELOG.md; then
+ if ! grep -qF "## [${VERSION}]" CHANGELOG.md; then
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if ! grep -q "## \[${VERSION}\]" CHANGELOG.md; then
if ! grep -qF "## [${VERSION}]" CHANGELOG.md; then
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/build-and-release.yml at line 25, The grep command in the
build-and-release workflow uses the VERSION variable as a regex pattern, which
can cause unintended matches if the version string contains regex
metacharacters. Add the -F flag to the grep command to force literal string
matching instead of regex interpretation. This ensures that the pattern "##
[${VERSION}]" is matched exactly as a literal string rather than being
interpreted as a regex pattern.

echo "Error: No entry found for version [${VERSION}] in CHANGELOG.md"
echo "Add a '## [${VERSION}]' section to CHANGELOG.md before releasing."
exit 1
fi
echo "CHANGELOG.md entry for [${VERSION}] verified."

build-server-windows:
runs-on: windows-latest
steps:
Expand Down Expand Up @@ -268,6 +287,7 @@ jobs:
contents: write
needs:
[
validate-changelog,
build-server-windows,
build-server-ubuntu,
build-server-macos,
Expand Down
144 changes: 144 additions & 0 deletions .github/workflows/release-prep.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
name: Release Prep

on:
workflow_dispatch:
inputs:
version:
description: "Version to release (X.Y.Z format — no v prefix)"
required: true
type: string

permissions:
contents: write
pull-requests: write

jobs:
release-prep:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify all mutable action refs in workflows
rg -nP 'uses:\s*[^@]+@v\d+(\.\d+)?(\.\d+)?\b' .github/workflows

Repository: AOSSIE-Org/PictoPy

Length of output: 4338


🏁 Script executed:

cat -n .github/workflows/release-prep.yml | sed -n '15,30p'

Repository: AOSSIE-Org/PictoPy

Length of output: 548


Pin GitHub Actions to immutable commit SHAs

Lines 20 and 25 use mutable tags (@v4). This weakens workflow supply-chain integrity because tag movement can change behavior without PR visibility.

Affected lines
20:        uses: actions/checkout@v4
25:        uses: actions/setup-node@v4

Replace with pinned commit SHAs, for example: actions/checkout@<full-commit-hash>

🧰 Tools
🪛 zizmor (1.25.2)

[error] 20-20: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release-prep.yml at line 20, The GitHub Actions workflow
is using mutable version tags (v4) for both actions/checkout and
actions/setup-node, which compromises supply-chain security since tags can be
moved to point to different commits without visibility. Replace the mutable tags
`@v4` with full immutable commit SHAs for both the actions/checkout action on line
20 and the actions/setup-node action on line 25. This ensures the workflow
always uses the exact intended version regardless of any tag changes.

Source: Linters/SAST tools

with:
fetch-depth: 0

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*

- name: Bump version across manifests
run: npm run version:bump -- ${{ inputs.version }}

Comment on lines +30 to +31

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Validate version once and stop interpolating raw ${{ inputs.version }} into shell commands.

Line 30 (and downstream lines in git/gh commands) expands user-provided workflow input directly inside run scripts. That enables shell injection before your Node script validation executes.

Suggested fix
+      - name: Validate release version
+        env:
+          RAW_VERSION: ${{ inputs.version }}
+        run: |
+          if [[ ! "$RAW_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
+            echo "Error: version must be X.Y.Z (no v prefix)"
+            exit 1
+          fi
+          echo "VERSION=$RAW_VERSION" >> "$GITHUB_ENV"
+
       - name: Bump version across manifests
-        run: npm run version:bump -- ${{ inputs.version }}
+        run: npm run version:bump -- "$VERSION"
...
       - name: Commit and push prep branch
         run: |
...
-          git checkout -b release/prep-${{ inputs.version }}
+          git checkout -b "release/prep-$VERSION"
...
-          git commit -m "chore: prepare release ${{ inputs.version }}"
-          git push origin release/prep-${{ inputs.version }}
+          git commit -m "chore: prepare release $VERSION"
+          git push origin "release/prep-$VERSION"
...
       - name: Open pull request
...
         run: |
           gh pr create \
-            --title "chore: release ${{ inputs.version }}" \
-            --body "## Release Prep: \`${{ inputs.version }}\`
+            --title "chore: release $VERSION" \
+            --body "## Release Prep: \`$VERSION\`
...
-          - Version bumped to \`${{ inputs.version }}\` across all manifest files
-          - CHANGELOG.md updated with a draft \`[${{ inputs.version }}]\` entry
+          - Version bumped to \`$VERSION\` across all manifest files
+          - CHANGELOG.md updated with a draft \`[$VERSION]\` entry
...
-          After merging, create the GitHub Release with tag \`v${{ inputs.version }}\` to trigger the build pipeline." \
+          After merging, create the GitHub Release with tag \`v$VERSION\` to trigger the build pipeline." \
             --base main \
-            --head release/prep-${{ inputs.version }} \
+            --head "release/prep-$VERSION" \
             --assignee ${{ github.actor }}

Also applies to: 117-120, 126-141

🧰 Tools
🪛 zizmor (1.25.2)

[error] 30-30: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release-prep.yml around lines 30 - 31, The workflow
expands the raw user-provided `inputs.version` directly into shell commands at
lines 30, 117-120, and 126-141, which creates a shell injection vulnerability.
Validate the version input early in the workflow before it is used in any run
scripts, then either pass the validated version as an environment variable to
the npm run version:bump command or have the Node script validate and read the
input directly rather than interpolating it raw into the shell command. This
ensures validation happens before any shell interpretation occurs and prevents
malicious shell syntax from being executed.

Source: Linters/SAST tools

- name: Generate CHANGELOG section
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ inputs.version }}
run: |
DATE=$(date -u +%Y-%m-%d)

# Find last stable release tag (ignore test/pre-release tags)
LAST_TAG=$(git tag -l --sort=-version:refname | grep -E '^v?[0-9]+\.[0-9]+\.[0-9]+$' | head -1)
echo "Last stable tag: ${LAST_TAG:-none}"

# Fetch PRs merged since last stable tag
if [ -n "$LAST_TAG" ]; then
SINCE_DATE=$(git log -1 --format=%aI "${LAST_TAG}")
PRS_JSON=$(gh pr list --state merged --search "merged:>${SINCE_DATE}" \
--json number,title,labels,author --limit 200)
else
PRS_JSON=$(gh pr list --state merged \
--json number,title,labels,author --limit 200)
fi

# Group PRs by Release Drafter label categories
ADDED=$(echo "$PRS_JSON" | jq -r '
.[] | select(.labels | map(.name) | any(. == "enhancement" or . == "UI"))
| "- \(.title) (#\(.number)) by @\(.author.login)"')

FIXED=$(echo "$PRS_JSON" | jq -r '
.[] | select(.labels | map(.name) | any(. == "bug"))
| "- \(.title) (#\(.number)) by @\(.author.login)"')

CHANGED=$(echo "$PRS_JSON" | jq -r '
.[] | select(.labels | map(.name) | any(. == "documentation"))
| "- \(.title) (#\(.number)) by @\(.author.login)"')

OTHER=$(echo "$PRS_JSON" | jq -r '
.[] | select(
(.labels | map(.name) | any(. == "enhancement" or . == "UI" or . == "bug" or . == "documentation"))
| not)
| "- \(.title) (#\(.number)) by @\(.author.login)"')

# Build section
SECTION="## [$VERSION] - $DATE

<!-- Auto-generated draft. Review and edit before merging. -->"

[ -n "$ADDED" ] && SECTION="$SECTION

### Added
$ADDED"
[ -n "$FIXED" ] && SECTION="$SECTION

### Fixed
$FIXED"
[ -n "$CHANGED" ] && SECTION="$SECTION

### Changed
$CHANGED"
[ -n "$OTHER" ] && SECTION="$SECTION

### Other
$OTHER"

echo "$SECTION" > /tmp/section.md

# Insert section after ## [Unreleased] in CHANGELOG.md
python3 << 'PYEOF'
with open('CHANGELOG.md', 'r') as f:
content = f.read()
with open('/tmp/section.md', 'r') as f:
section = f.read().strip()
marker = '## [Unreleased]'
idx = content.find(marker)
if idx == -1:
raise SystemExit('Error: [Unreleased] header not found in CHANGELOG.md')
insert_pos = idx + len(marker)
new_content = content[:insert_pos] + '\n\n' + section + '\n' + content[insert_pos:]
with open('CHANGELOG.md', 'w') as f:
f.write(new_content)
Comment on lines +97 to +109

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard against duplicate ## [X.Y.Z] sections on reruns.

The insertion logic always appends after ## [Unreleased]. Re-running the same version creates duplicate headers, which degrades changelog consistency and review clarity.

Suggested fix
       - name: Generate CHANGELOG section
         env:
           GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
           VERSION: ${{ inputs.version }}
         run: |
...
           # Insert section after ## [Unreleased] in CHANGELOG.md
           python3 << 'PYEOF'
+          import os
           with open('CHANGELOG.md', 'r') as f:
               content = f.read()
           with open('/tmp/section.md', 'r') as f:
               section = f.read().strip()
+          version = os.environ['VERSION']
+          header = f'## [{version}]'
+          if header in content:
+              raise SystemExit(f'Error: {header} already exists in CHANGELOG.md')
           marker = '## [Unreleased]'
           idx = content.find(marker)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
python3 << 'PYEOF'
with open('CHANGELOG.md', 'r') as f:
content = f.read()
with open('/tmp/section.md', 'r') as f:
section = f.read().strip()
marker = '## [Unreleased]'
idx = content.find(marker)
if idx == -1:
raise SystemExit('Error: [Unreleased] header not found in CHANGELOG.md')
insert_pos = idx + len(marker)
new_content = content[:insert_pos] + '\n\n' + section + '\n' + content[insert_pos:]
with open('CHANGELOG.md', 'w') as f:
f.write(new_content)
python3 << 'PYEOF'
import os
with open('CHANGELOG.md', 'r') as f:
content = f.read()
with open('/tmp/section.md', 'r') as f:
section = f.read().strip()
version = os.environ['VERSION']
header = f'## [{version}]'
if header in content:
raise SystemExit(f'Error: {header} already exists in CHANGELOG.md')
marker = '## [Unreleased]'
idx = content.find(marker)
if idx == -1:
raise SystemExit('Error: [Unreleased] header not found in CHANGELOG.md')
insert_pos = idx + len(marker)
new_content = content[:insert_pos] + '\n\n' + section + '\n' + content[insert_pos:]
with open('CHANGELOG.md', 'w') as f:
f.write(new_content)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release-prep.yml around lines 97 - 109, The Python script
that inserts changelog content after the `## [Unreleased]` marker does not check
whether a release section header (such as `## [X.Y.Z]`) already exists in the
changelog, causing duplicate sections on workflow reruns. Extract the version
header from the section variable being inserted, search the existing content for
that same header, and only proceed with the insertion if the header is not
already present. This ensures idempotency when the release-prep workflow runs
multiple times for the same version.

print('CHANGELOG.md updated.')
PYEOF

- name: Commit and push prep branch
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git checkout -b release/prep-${{ inputs.version }}
git add package.json frontend/package.json frontend/src-tauri/Cargo.toml CHANGELOG.md
git commit -m "chore: prepare release ${{ inputs.version }}"
git push origin release/prep-${{ inputs.version }}

- name: Open pull request
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh pr create \
--title "chore: release ${{ inputs.version }}" \
--body "## Release Prep: \`${{ inputs.version }}\`

Auto-generated by the release prep workflow.

### What's included
- Version bumped to \`${{ inputs.version }}\` across all manifest files
- CHANGELOG.md updated with a draft \`[${{ inputs.version }}]\` entry

### Before merging
- [ ] Review and clean up the CHANGELOG entry
- [ ] Verify version strings in \`package.json\`, \`frontend/package.json\`, and \`frontend/src-tauri/Cargo.toml\`
- [ ] Run \`cargo check\` in \`frontend/src-tauri/\` to regenerate \`Cargo.lock\`

After merging, create the GitHub Release with tag \`v${{ inputs.version }}\` to trigger the build pipeline." \
--base main \
--head release/prep-${{ inputs.version }} \
--assignee ${{ github.actor }}
66 changes: 66 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Changelog

All notable changes to PictoPy will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [1.1.0] - 2026-05-22

### Added

- Settings page with folder and user preference management ([#606](https://github.com/AOSSIE-Org/PictoPy/pull/606), [#516](https://github.com/AOSSIE-Org/PictoPy/pull/516))
- Memories backend implementation ([#777](https://github.com/AOSSIE-Org/PictoPy/pull/777))
- Global face reclustering option with backend API and UI support ([#560](https://github.com/AOSSIE-Org/PictoPy/pull/560))
- Centralized logging system ([#548](https://github.com/AOSSIE-Org/PictoPy/pull/548))
- Live progress tracking with polling and animated progress bar ([#574](https://github.com/AOSSIE-Org/PictoPy/pull/574))
- Scrollbar timeline with month-year markers for Home and AI Tagging pages ([#552](https://github.com/AOSSIE-Org/PictoPy/pull/552))
- Empty state placeholders for AI tagging and home pages ([#549](https://github.com/AOSSIE-Org/PictoPy/pull/549))
- Camera selection for the webcam component ([#624](https://github.com/AOSSIE-Org/PictoPy/pull/624))
- Advanced zoom-on-scroll and panning logic in image preview ([#530](https://github.com/AOSSIE-Org/PictoPy/pull/530), [#835](https://github.com/AOSSIE-Org/PictoPy/pull/835))
- Slideshow looping for seamless playback ([#1100](https://github.com/AOSSIE-Org/PictoPy/pull/1100))
- Smooth exit animation for the image details panel ([#978](https://github.com/AOSSIE-Org/PictoPy/pull/978))
- Face renaming via Enter key ([#581](https://github.com/AOSSIE-Org/PictoPy/pull/581))
- Spawning and closing of backend and sync microservice from the Tauri app itself ([#1009](https://github.com/AOSSIE-Org/PictoPy/pull/1009))
- Arch Linux AUR package with automated publishing workflow ([#1268](https://github.com/AOSSIE-Org/PictoPy/pull/1268))
- AI models-based app size optimization ([#1263](https://github.com/AOSSIE-Org/PictoPy/pull/1263))

### Changed

- Streamlined Rust backend to a minimal API and updated docs structure ([#515](https://github.com/AOSSIE-Org/PictoPy/pull/515))
- Extended hover activation hotspot for navigation arrows ([#702](https://github.com/AOSSIE-Org/PictoPy/pull/702))
- Reversed marquee animation direction ([#894](https://github.com/AOSSIE-Org/PictoPy/pull/894))
- Improved gallery hover interaction with hover delay and cursor cleanup ([#715](https://github.com/AOSSIE-Org/PictoPy/pull/715))
- Switched backend setup to use Conda ([#975](https://github.com/AOSSIE-Org/PictoPy/pull/975))
- Separated backend services and changed server port numbers ([#933](https://github.com/AOSSIE-Org/PictoPy/pull/933), [#934](https://github.com/AOSSIE-Org/PictoPy/pull/934))
- Simplified Redux state ([#599](https://github.com/AOSSIE-Org/PictoPy/pull/599))

### Fixed

- Validated and sanitized image_ids in album APIs to prevent empty/invalid input and unsafe IN clauses ([#1253](https://github.com/AOSSIE-Org/PictoPy/pull/1253), [#629](https://github.com/AOSSIE-Org/PictoPy/pull/629))
- Prevented InfoDialog from flickering to blue variant on close ([#630](https://github.com/AOSSIE-Org/PictoPy/pull/630))
- Prevented duplicate scrollbars on Windows/Tauri ([#941](https://github.com/AOSSIE-Org/PictoPy/pull/941))
- Enabled "Open Folder" button functionality ([#976](https://github.com/AOSSIE-Org/PictoPy/pull/976))
- Fixed infinite recursion in InterceptHandler logging ([#940](https://github.com/AOSSIE-Org/PictoPy/pull/940))
- Fixed Windows build not opening on first launch ([#922](https://github.com/AOSSIE-Org/PictoPy/pull/922))
- Fixed macOS build not opening ([#659](https://github.com/AOSSIE-Org/PictoPy/pull/659))
- Fixed progress bar overflow in onboarding steps ([#726](https://github.com/AOSSIE-Org/PictoPy/pull/726))
- Improved face clustering accuracy with a similarity threshold ([#771](https://github.com/AOSSIE-Org/PictoPy/pull/771))
- Fixed atomic DB operations and consistency in global reclustering ([#570](https://github.com/AOSSIE-Org/PictoPy/pull/570))
- Fixed 500 error in the `/cluster_id/images` API ([#598](https://github.com/AOSSIE-Org/PictoPy/pull/598))
- Fixed critical database connection leaks ([#547](https://github.com/AOSSIE-Org/PictoPy/pull/547))
- Fixed database consistency on external image deletion events ([#520](https://github.com/AOSSIE-Org/PictoPy/pull/520))
- Fixed "Open Original File" button in the image details panel ([#542](https://github.com/AOSSIE-Org/PictoPy/pull/542))
- Fixed text overflow in the image details panel ([#537](https://github.com/AOSSIE-Org/PictoPy/pull/537))
- Fixed oversized navbar height on first load ([#692](https://github.com/AOSSIE-Org/PictoPy/pull/692))
- Fixed hover state and search bar UI issues ([#532](https://github.com/AOSSIE-Org/PictoPy/pull/532), [#529](https://github.com/AOSSIE-Org/PictoPy/pull/529))
- Fixed image lock error ([#565](https://github.com/AOSSIE-Org/PictoPy/pull/565))

### Documentation

- Added CI check for markdown linting ([#1266](https://github.com/AOSSIE-Org/PictoPy/pull/1266))
- Restructured intro page and sidebar navigation ([#1264](https://github.com/AOSSIE-Org/PictoPy/pull/1264))
- Improved documentation site responsiveness on mobile devices ([#601](https://github.com/AOSSIE-Org/PictoPy/pull/601), [#763](https://github.com/AOSSIE-Org/PictoPy/pull/763))
- Added Miniconda installation instructions to the Manual Setup Guide ([#971](https://github.com/AOSSIE-Org/PictoPy/pull/971))
16 changes: 16 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,22 @@ cargo check

> **Note:** The `--` separator between `version:bump` and the version number is required by npm to forward the argument to the underlying script.

### Automated release prep

When a release is approaching, trigger the release prep workflow from the GitHub Actions tab:

1. Go to **Actions → Release Prep → Run workflow**
2. Enter the target version in `X.Y.Z` format (no `v` prefix)
3. The workflow will open a PR that includes:
- Version bumped across all manifest files
- A draft `CHANGELOG.md` entry scaffolded from merged PRs since the last release, grouped by label
4. Review the PR, clean up the CHANGELOG entry prose, and merge
5. Create the GitHub Release with tag `v<version>` to trigger the build pipeline

### Release validation (CI gate)

`build-and-release.yml` includes a `validate-changelog` job that runs on every release trigger. It checks that `CHANGELOG.md` contains a `## [X.Y.Z]` entry matching the release tag before the publish step is allowed to proceed. If the entry is missing, the release is blocked. Always merge the release prep PR before creating the GitHub Release.

## Additional Resources

- [Tauri Documentation](https://tauri.app/start/)
Expand Down
Loading