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
9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,15 @@ jobs:
- name: Validate Sparkle appcast tooling
run: python3 scripts/sparkle_appcast.py self-test

- name: Validate release metadata
run: python3 scripts/validate_release_metadata.py

- name: Validate release-ready PR title
if: ${{ github.event_name == 'pull_request' }}
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: python3 scripts/validate_pr_title.py "$PR_TITLE"

- name: Lint Swift formatting
run: xcrun swift-format lint --strict --recursive Sources Tests Package.swift

Expand Down
117 changes: 113 additions & 4 deletions .github/workflows/package-release.yml
Original file line number Diff line number Diff line change
@@ -1,22 +1,86 @@
name: package-release

on:
workflow_dispatch:
workflow_call:
inputs:
artifact_name:
description: Artifact name prefix
required: false
type: string
default: agentd
configuration:
description: Swift build configuration
required: false
type: string
default: release
checkout_ref:
description: Git ref to package
required: false
type: string
default: ""
release_tag:
description: GitHub release tag to attach assets to
required: false
type: string
default: ""
sparkle_download_url:
description: HTTPS URL where the notarized agentd.zip update archive will be hosted
required: false
type: string
default: ""
upload_release_assets:
description: Upload packaged artifacts to release_tag
required: false
type: boolean
default: false
secrets:
AGENTD_CODESIGN_CERTIFICATE_P12:
required: true
AGENTD_CODESIGN_CERTIFICATE_PASSWORD:
required: true
AGENTD_CODESIGN_IDENTITY:
required: true
AGENTD_NOTARY_APPLE_ID:
required: true
AGENTD_NOTARY_TEAM_ID:
required: true
AGENTD_NOTARY_PASSWORD:
required: true
AGENTD_SPARKLE_FEED_URL:
required: true
AGENTD_SPARKLE_PUBLIC_ED_KEY:
required: true
AGENTD_SPARKLE_PRIVATE_ED_KEY:
required: true
workflow_dispatch:
inputs:
artifact_name:
description: Artifact name prefix
required: false
default: agentd
configuration:
description: Swift build configuration
required: false
default: release
sparkle_download_url:
description: HTTPS URL where the notarized agentd.zip update archive will be hosted
required: false
checkout_ref:
description: Git ref to package
required: false
default: ""
release_tag:
description: GitHub release tag to attach assets to
required: false
default: ""
upload_release_assets:
description: Upload packaged artifacts to release_tag after packaging
required: false
type: boolean
default: false

permissions:
contents: read
contents: write

jobs:
package-release:
Expand All @@ -25,6 +89,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v5
with:
ref: ${{ inputs.checkout_ref || inputs.release_tag || github.ref }}

- name: Select Swift 6 toolchain
run: |
Expand All @@ -42,6 +108,32 @@ jobs:
done
swift --version

- name: Resolve release metadata
id: release
env:
INPUT_RELEASE_TAG: ${{ inputs.release_tag }}
INPUT_SPARKLE_DOWNLOAD_URL: ${{ inputs.sparkle_download_url }}
run: |
set -euo pipefail
release_tag="$INPUT_RELEASE_TAG"
sparkle_download_url="$INPUT_SPARKLE_DOWNLOAD_URL"
if [[ -z "$sparkle_download_url" && -n "$release_tag" ]]; then
sparkle_download_url="https://github.com/${GITHUB_REPOSITORY}/releases/download/${release_tag}/agentd.zip"
fi
if [[ -z "$sparkle_download_url" ]]; then
echo "::error::Provide sparkle_download_url or release_tag so Sparkle can verify the final archive URL."
exit 1
fi
release_notes_url=""
if [[ -n "$release_tag" ]]; then
release_notes_url="https://github.com/${GITHUB_REPOSITORY}/releases/tag/${release_tag}"
fi
{
echo "release_tag=$release_tag"
echo "sparkle_download_url=$sparkle_download_url"
echo "release_notes_url=$release_notes_url"
} >> "$GITHUB_OUTPUT"

- name: Require release secrets
env:
AGENTD_CODESIGN_CERTIFICATE_P12: ${{ secrets.AGENTD_CODESIGN_CERTIFICATE_P12 }}
Expand All @@ -53,7 +145,7 @@ jobs:
AGENTD_SPARKLE_FEED_URL: ${{ secrets.AGENTD_SPARKLE_FEED_URL }}
AGENTD_SPARKLE_PUBLIC_ED_KEY: ${{ secrets.AGENTD_SPARKLE_PUBLIC_ED_KEY }}
AGENTD_SPARKLE_PRIVATE_ED_KEY: ${{ secrets.AGENTD_SPARKLE_PRIVATE_ED_KEY }}
AGENTD_SPARKLE_DOWNLOAD_URL: ${{ inputs.sparkle_download_url }}
AGENTD_SPARKLE_DOWNLOAD_URL: ${{ steps.release.outputs.sparkle_download_url }}
run: |
set -euo pipefail
missing=0
Expand Down Expand Up @@ -117,7 +209,8 @@ jobs:
AGENTD_NOTARY_PASSWORD: ${{ secrets.AGENTD_NOTARY_PASSWORD }}
AGENTD_SPARKLE_FEED_URL: ${{ secrets.AGENTD_SPARKLE_FEED_URL }}
AGENTD_SPARKLE_PUBLIC_ED_KEY: ${{ secrets.AGENTD_SPARKLE_PUBLIC_ED_KEY }}
AGENTD_SPARKLE_DOWNLOAD_URL: ${{ inputs.sparkle_download_url }}
AGENTD_SPARKLE_DOWNLOAD_URL: ${{ steps.release.outputs.sparkle_download_url }}
AGENTD_SPARKLE_RELEASE_NOTES_URL: ${{ steps.release.outputs.release_notes_url }}
AGENTD_SPARKLE_ED_KEY_FILE: ${{ runner.temp }}/sparkle-ed25519.key
AGENTD_SPARKLE_REQUIRE_SIGNED_FEED: "1"
run: scripts/package_app.sh
Expand Down Expand Up @@ -149,3 +242,19 @@ jobs:
dist/codesign.txt
dist/spctl.txt
if-no-files-found: error

- name: Upload GitHub release assets
if: ${{ (inputs.upload_release_assets == true || inputs.upload_release_assets == 'true') && inputs.release_tag != '' }}
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ inputs.release_tag }}
run: |
set -euo pipefail
gh release upload "$RELEASE_TAG" \
dist/agentd.zip \
dist/SHA256SUMS \
dist/update-channel.json \
dist/appcast.xml \
dist/codesign.txt \
dist/spctl.txt \
--clobber
47 changes: 47 additions & 0 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: release-please

on:
push:
branches:
- main
workflow_dispatch:

permissions:
contents: write
issues: write
pull-requests: write

concurrency:
group: release-please-${{ github.ref }}
cancel-in-progress: false

jobs:
release-please:
name: release please
runs-on: ubuntu-latest
outputs:
release_created: ${{ steps.release.outputs.release_created }}
tag_name: ${{ steps.release.outputs.tag_name }}
steps:
- name: Create release PR or GitHub release
id: release
uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0
with:
token: ${{ secrets.AGENTD_RELEASE_TOKEN || secrets.GITHUB_TOKEN }}
config-file: release-please-config.json
manifest-file: .release-please-manifest.json
target-branch: main

package-release:
name: package release
needs: release-please
if: ${{ needs.release-please.outputs.release_created == 'true' }}
uses: ./.github/workflows/package-release.yml
permissions:
contents: write
secrets: inherit
with:
checkout_ref: ${{ needs.release-please.outputs.tag_name }}
release_tag: ${{ needs.release-please.outputs.tag_name }}
sparkle_download_url: https://github.com/${{ github.repository }}/releases/download/${{ needs.release-please.outputs.tag_name }}/agentd.zip
upload_release_assets: true
3 changes: 3 additions & 0 deletions .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
".": "0.0.2"
}
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Changelog

## [0.0.2](https://github.com/evalops/agentd/compare/v0.0.1...v0.0.2) (2026-05-01)

Release v0.0.2 exercised Sparkle updates from v0.0.1 with notarized GitHub
release artifacts, a signed feed, and a signed update archive.

## 0.0.1 (2026-05-01)

Initial signed and notarized EvalOps agentd release with Sparkle update assets
published on GitHub Releases.
27 changes: 19 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,10 +180,14 @@ Application identity. To notarize and staple the bundle, either set
`AGENTD_NOTARY_APPLE_ID`, `AGENTD_NOTARY_TEAM_ID`, and
`AGENTD_NOTARY_PASSWORD`.

The `package-release` GitHub Actions workflow performs the credential-backed
release path and uploads the stapled app, archive, checksums, codesign details,
and Gatekeeper assessment. Configure these repository secrets before dispatching
it:
Release Please opens version PRs from release-ready Conventional Commit PR
titles. Merging a Release Please PR creates the GitHub release; the
`release-please` workflow then calls `package-release` to build the notarized
app, generate the signed Sparkle appcast, upload evidence artifacts, and attach
the release assets. Release PRs keep `CHANGELOG.md`, `version.txt`,
`.release-please-manifest.json`, and both `support/Info.plist` version fields
in sync. Configure these repository secrets before relying on the automated
release lane:

- `AGENTD_CODESIGN_CERTIFICATE_P12`: base64-encoded Developer ID Application
`.p12`.
Expand All @@ -197,10 +201,17 @@ it:
- `AGENTD_SPARKLE_PUBLIC_ED_KEY`
- `AGENTD_SPARKLE_PRIVATE_ED_KEY`: the private key text exported with Sparkle's
`generate_keys -x`; store it as a secret and rotate it if exposed.

The workflow also requires a `sparkle_download_url` dispatch input. It must be
the final HTTPS URL where the uploaded `agentd.zip` will be served, because
Sparkle validates the appcast enclosure URL before downloading the update.
- `AGENTD_RELEASE_TOKEN`: optional fine-grained PAT used by Release Please.
Configure it when release PR branches need normal `pull_request` CI runs;
otherwise the workflow falls back to `GITHUB_TOKEN`.

For manual packaging, dispatch `package-release` with either `release_tag` or
`sparkle_download_url`. `release_tag` derives the final
`https://github.com/evalops/agentd/releases/download/<tag>/agentd.zip` URL and
can also attach the generated release assets when `upload_release_assets` is
enabled. A direct `sparkle_download_url` must be the final HTTPS URL where the
uploaded `agentd.zip` will be served, because Sparkle validates the appcast
enclosure URL before downloading the update.

`scripts/permission_smoke.sh` packages the app when needed, installs the tested
bundle to `/Applications/EvalOps agentd.app` by default, records macOS
Expand Down
24 changes: 23 additions & 1 deletion docs/release-update-channel.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ The signed update-channel path is intentionally evidence-first:
`dist/appcast.xml`.
7. Publish `dist/agentd.zip`, `dist/appcast.xml`, `dist/SHA256SUMS`,
`dist/update-channel.json`, `dist/codesign.txt`, and `dist/spctl.txt` as
release evidence.
release evidence and GitHub release assets.
8. Publish update metadata only after the archive checksum, Sparkle signature,
signing identity, notarization request id, and Gatekeeper output are recorded.

Expand Down Expand Up @@ -50,6 +50,28 @@ Sparkle release configuration is injected at package time:
automatic installation of major upgrades.
- `AGENTD_SPARKLE_CRITICAL_UPDATE=1`: marks the release as critical.

## Automated Releases

`release-please` runs on every push to `main`. It reads Conventional Commit PR
titles, opens a release PR that updates `CHANGELOG.md`,
`.release-please-manifest.json`, `version.txt`, `support/Info.plist`
`CFBundleVersion`, and `support/Info.plist` `CFBundleShortVersionString`, then
creates the GitHub release when that PR merges. The same workflow calls
`package-release` with the new tag so the notarized app, signed Sparkle archive,
appcast, checksums, codesign evidence, Gatekeeper assessment, and
`update-channel.json` are attached to that release automatically.

The CI workflow validates PR titles with `scripts/validate_pr_title.py` because
non-Conventional titles are invisible to Release Please. Use titles such as
`feat: add activity summaries`, `fix: recover capture startup`, or
`chore: update signing evidence`.

For bot-created release PR branches to trigger normal PR checks, configure the
optional `AGENTD_RELEASE_TOKEN` secret with a fine-grained token that can push
branches and open pull requests in this repository. Without it, Release Please
falls back to `GITHUB_TOKEN`; that can still update GitHub state, but events
created by `GITHUB_TOKEN` may not start follow-on workflows.

For local appcast fixture testing without notarization, set
`AGENTD_SPARKLE_ALLOW_UNNOTARIZED=1`; do not use that override in release
automation.
Expand Down
22 changes: 22 additions & 0 deletions release-please-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"include-component-in-tag": false,
"packages": {
".": {
"package-name": "agentd",
"release-type": "simple",
"changelog-path": "CHANGELOG.md",
"extra-files": [
{
"type": "xml",
"path": "support/Info.plist",
"xpath": "/plist/dict/key[text()='CFBundleVersion']/following-sibling::string[1]"
},
{
"type": "xml",
"path": "support/Info.plist",
"xpath": "/plist/dict/key[text()='CFBundleShortVersionString']/following-sibling::string[1]"
}
]
}
}
}
45 changes: 45 additions & 0 deletions scripts/validate_pr_title.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#!/usr/bin/env python3
"""Validate PR titles are usable by release automation."""

from __future__ import annotations

import re
import sys


ALLOWED_TYPES = (
"feat",
"fix",
"chore",
"docs",
"test",
"refactor",
"perf",
"build",
"ci",
"revert",
"release",
)

PATTERN = re.compile(
rf"^({'|'.join(ALLOWED_TYPES)})(\([A-Za-z0-9._/-]+\))?!?: .+"
)


def main() -> int:
title = " ".join(sys.argv[1:]).strip()
if not title:
raise SystemExit("PR title is empty")
if PATTERN.match(title):
print(f"Release-ready PR title: {title}")
return 0
allowed = ", ".join(ALLOWED_TYPES)
raise SystemExit(
"PR title must use Conventional Commits so release-please can "
f"version agentd automatically. Use one of: {allowed}. "
"Example: feat: add activity summaries"
)


if __name__ == "__main__":
sys.exit(main())
Loading