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
19 changes: 13 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -175,19 +175,26 @@ jobs:
- name: Install XcodeGen
run: brew install xcodegen

- name: Generate the project
run: xcodegen generate
- name: Generate the project and reject drift
run: scripts/check_xcode_project.sh

# This compiles a different product graph and activation path from the Developer ID build. Ad-hoc signing
# deliberately strips protected entitlements; archive signing validates those with the distribution profiles.
# This compiles a different product graph and activation path from the Developer ID build. The build clears
# protected entitlements because CI has no Store profiles; the next step re-signs the isolated product with the
# repository plists so the product gate still inspects the actual executable entitlements it is meant to police.
- name: Build the Mac App Store product
run: |
xcodebuild build -project Flowlight.xcodeproj -scheme "Flowlight App Store" \
-configuration AppStore -destination 'generic/platform=macOS' -derivedDataPath "$RUNNER_TEMP/store-build" \
CODE_SIGN_IDENTITY=- CODE_SIGN_STYLE=Manual DEVELOPMENT_TEAM= CODE_SIGN_ENTITLEMENTS=

- name: Check the Mac App Store product structure
run: scripts/check_app_store_product.sh "$RUNNER_TEMP/store-build/Build/Products/AppStore/Flowlight.app"
- name: Check the Mac App Store product structure and signed entitlements
run: |
set -euo pipefail
APP="$RUNNER_TEMP/store-build/Build/Products/AppStore/Flowlight.app"
EXTENSION="$APP/Contents/PlugIns/com.flowlight.app.filter.appex"
codesign --force --sign - --entitlements FlowlightExtension/FlowlightExtension-AppStore.entitlements "$EXTENSION"
codesign --force --sign - --entitlements Flowlight/Flowlight-AppStore.entitlements "$APP"
scripts/check_app_store_product.sh "$APP"

# Ad-hoc signing: the entitlements that need a Developer ID profile are only exercised by the release build.
- name: Test
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ jobs:
# CodeQL something to trace, and tracing it twice would double the cost for the same findings.
sudo xcode-select -s "$(ls -d /Applications/Xcode*.app | sort -V | tail -1)"
xcodebuild -version
brew install xcodegen
xcodegen generate
# Analyze the exact committed graph that CI reviewed and Xcode Cloud discovers.
xcodebuild -project Flowlight.xcodeproj -list >/dev/null

- uses: github/codeql-action/init@7999b86c43a865dc79d8923397f35af22de63401 # v4
with:
Expand Down
45 changes: 30 additions & 15 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
name: Release

# Builds, signs, notarizes and publishes on a version tag:
#
# git tag -a v0.2.3 -m "Flowlight 0.2.3" && git push origin v0.2.3
#
# The secrets this needs are listed in docs/DEVELOPMENT.md, along with what it means to keep a signing identity in
# A version-tag push builds, signs, notarizes and verifies without publishing. After that dry run passes, dispatch
# this workflow for the existing tag with dry_run=false to publish it. The secrets this needs are listed in
# docs/DEVELOPMENT.md, along with what it means to keep a signing identity in
# GitHub. Without them the job fails at the import step rather than publishing something unsigned.
#
# It runs without waiting for an approval. What gates it instead: the release environment only accepts v* tags,
Expand Down Expand Up @@ -66,9 +64,13 @@ jobs:
- name: Read the version and check it matches the tag
id: version
run: |
set -euo pipefail
VERSION=$(sed -n 's/.*MARKETING_VERSION:[[:space:]]*"\(.*\)".*/\1/p' project.yml)
BUILD=$(sed -n 's/.*CURRENT_PROJECT_VERSION:[[:space:]]*"\(.*\)".*/\1/p' project.yml)
TAG="${{ github.event.inputs.tag || github.ref_name }}"
[[ -n "$VERSION" && -n "$BUILD" ]] || { echo "::error::Missing version metadata in project.yml."; exit 1; }
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "build=$BUILD" >> "$GITHUB_OUTPUT"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
if [[ "$TAG" != "v$VERSION" ]]; then
echo "::error::Tag $TAG doesn't match MARKETING_VERSION $VERSION in project.yml."
Expand All @@ -82,15 +84,28 @@ jobs:
sudo xcode-select -s "$(ls -d /Applications/Xcode*.app | sort -V | tail -1)"
xcodebuild -version

- name: Install XcodeGen
run: brew install xcodegen

- name: Generate the project
run: xcodegen generate
# The committed project is the exact graph reviewed by CI and selected by Xcode Cloud. Verify its release
# metadata before consuming it so a stale project can never publish under a newer project.yml/tag.
- name: Check committed project version
run: |
set -euo pipefail
for target in Flowlight FlowlightAppStore; do
settings="$RUNNER_TEMP/$target-settings.txt"
xcodebuild -project Flowlight.xcodeproj -target "$target" -configuration Release -showBuildSettings > "$settings"
project_version=$(sed -n 's/^[[:space:]]*MARKETING_VERSION = //p' "$settings")
project_build=$(sed -n 's/^[[:space:]]*CURRENT_PROJECT_VERSION = //p' "$settings")
if [[ "$project_version" != "${{ steps.version.outputs.version }}" || "$project_build" != "${{ steps.version.outputs.build }}" ]]; then
echo "::error::$target committed project is $project_version/$project_build; project.yml is ${{ steps.version.outputs.version }}/${{ steps.version.outputs.build }}."
exit 1
fi
done

- name: Test
timeout-minutes: 40
run: |
# GitHub starts run blocks with bash -e. This step records the first attempt's status so it can retry a
# timeout, then exits explicitly if the final attempt failed.
set +e
set -uo pipefail
# xcodebuild occasionally never returns here — the same suite passes locally and on most runs, and the
# per-test allowance below doesn't always catch it, so the whole invocation gets a watchdog. One retry:
Expand Down Expand Up @@ -227,7 +242,7 @@ jobs:
( cd build && shasum -a 256 Flowlight.dmg "Flowlight-$VERSION.pkg" | tee SHA256SUMS.txt )

- name: Publish the release
if: ${{ github.event.inputs.dry_run != 'true' }}
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.dry_run != 'true' }}
env:
GH_TOKEN: ${{ github.token }}
run: |
Expand Down Expand Up @@ -261,7 +276,7 @@ jobs:
# was invisible because verification used to pass when it found nothing. Checked against the published
# release rather than the build directory, because what matters is what people can actually download.
- name: The release must carry the checksums the updater looks for
if: ${{ github.event.inputs.dry_run != 'true' }}
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.dry_run != 'true' }}
env:
GH_TOKEN: ${{ github.token }}
run: |
Expand All @@ -285,7 +300,7 @@ jobs:

# The cask points at the release that was just published, so this runs last.
- name: Update the Homebrew cask
if: ${{ env.TAP_TOKEN != '' && github.event.inputs.dry_run != 'true' }}
if: ${{ env.TAP_TOKEN != '' && github.event_name == 'workflow_dispatch' && github.event.inputs.dry_run != 'true' }}
run: |
set -euo pipefail
git clone "https://x-access-token:$TAP_TOKEN@github.com/xinbetween/homebrew-tap.git" "$RUNNER_TEMP/tap"
Expand All @@ -295,7 +310,7 @@ jobs:
git -C "$RUNNER_TEMP/tap" push

- name: Say what a dry run produced
if: ${{ github.event.inputs.dry_run == 'true' }}
if: ${{ github.event_name == 'push' || github.event.inputs.dry_run == 'true' }}
run: |
set -euo pipefail
VERSION="${{ steps.version.outputs.version }}"
Expand All @@ -306,6 +321,6 @@ jobs:
echo '```'
cat build/SHA256SUMS.txt
echo '```'
echo "Re-run without **dry_run** to publish \`${{ steps.version.outputs.tag }}\`."
echo "Dispatch this workflow for \`${{ steps.version.outputs.tag }}\` with **dry_run=false** to publish."
} >> "$GITHUB_STEP_SUMMARY"
echo "Dry run complete — Flowlight $VERSION built and verified, nothing published."
15 changes: 13 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,19 @@ xcuserdata/
*.pkg
*.dmg

# Generated by XcodeGen (regenerate with `xcodegen generate`)
Flowlight.xcodeproj/
# Generated by XcodeGen (regenerate with `xcodegen generate`). The shared project
# and schemes stay committed because Xcode Cloud must discover its selected
# container before any post-clone script can run. Local and Xcode-managed state
# remains ignored.
Flowlight.xcodeproj/*
!Flowlight.xcodeproj/project.pbxproj
!Flowlight.xcodeproj/project.xcworkspace/
Flowlight.xcodeproj/project.xcworkspace/*
!Flowlight.xcodeproj/project.xcworkspace/contents.xcworkspacedata
!Flowlight.xcodeproj/xcshareddata/
Flowlight.xcodeproj/xcshareddata/*
!Flowlight.xcodeproj/xcshareddata/xcschemes/
!Flowlight.xcodeproj/xcshareddata/xcschemes/*.xcscheme

# Signing material. Never commit any of this, however briefly it lives here.
certs/
Expand Down
19 changes: 9 additions & 10 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ Thanks for helping make network activity on macOS understandable.
## Getting started

```sh
brew install xcodegen
xcodegen generate
brew install xcodegen # project regeneration requires exactly 2.46.0
scripts/check_xcode_project.sh --write
scripts/check_xcode_project.sh
scripts/build-local.sh
open build/Build/Products/Release/Flowlight.app --args -FLDemo YES # synthetic data, safe for screenshots
```
Expand Down Expand Up @@ -67,11 +68,9 @@ gh pr create --base main --title "Flowlight 0.13.4"
# wait for review and every required PR check to pass

git tag -a v0.13.4 -m "Flowlight 0.13.4"
git push origin v0.13.4
# cancel the automatic publish run, then exercise the immutable tag first:
gh workflow run Release --ref v0.13.4 -f dry_run=true
# inspect the successful signed/notarized/Gatekeeper dry run, then publish from the same tag:
gh workflow run Release --ref v0.13.4
git push origin v0.13.4 # automatically runs the signed dry run; it cannot publish
# inspect that successful signed/notarized/Gatekeeper run, then publish from the same tag:
gh workflow run Release --ref v0.13.4 -f tag=v0.13.4
# verify the GitHub release, DMG, PKG and SHA256SUMS.txt before the final merge
gh pr merge --merge --delete-branch
```
Expand All @@ -84,9 +83,9 @@ will be signed. Publishing before merging keeps the GitHub Pages site from annou
`docs/` is served from `main`, its Download button resolves to the latest GitHub release, CI requires `docs/` to match
`site/`, and the site builder reads `MARKETING_VERSION`.

A tag push starts `release.yml`; cancel that automatic publication before it reaches its publish step and dispatch the
dry run from the immutable tag. A dry run must prove tests, signing, notarization and Gatekeeper verification before a
production dispatch is allowed. After production succeeds, verify the release page and downloaded DMG, PKG and
A tag push starts `release.yml` in dry-run-only mode; push-triggered runs cannot publish. It must prove tests, signing,
notarization and Gatekeeper verification before an explicit production dispatch for that immutable tag. After production
succeeds, verify the release page and downloaded DMG, PKG and
`SHA256SUMS.txt` checksums, then merge the release PR into `main`.

[docs/DEVELOPMENT.md](docs/DEVELOPMENT.md#release-branches) documents the signing inputs and workflow internals.
Expand Down
Loading
Loading