Skip to content
Closed
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
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -201,9 +201,12 @@ jobs:
scripts/test-rule-pack-policy.sh
scripts/test-swift-warnings.sh
scripts/test-codeql-gate.sh
scripts/test-update-cask.sh
python3 -I scripts/test_classify_changes.py
python3 -I scripts/test_check_site.py
python3 scripts/test_release_checks.py
python3 -I scripts/test_release_checks.py
python3 -I scripts/test_release_artifact.py
python3 -I scripts/test_release_workflow.py

# Ad-hoc signing: the entitlements that need a Developer ID profile are only exercised by the release build.
- name: Test
Expand Down
551 changes: 430 additions & 121 deletions .github/workflows/release.yml

Large diffs are not rendered by default.

28 changes: 16 additions & 12 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Internals are documented in [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md).
Every release follows this non-negotiable sequence:

```text
feature branch → release branch → reviewed green PR → immutable tag → signed dry run → publish and verify → merge to main
feature branch → release branch → reviewed green PR → immutable tag → signed preparation → publish exact bytes and verify → merge to main
```

`main` is what has shipped, not what is being built. Start each feature from it and rebase it onto it; never merge
Expand All @@ -65,28 +65,32 @@ git merge --ff-only feature/relaunch-through-proxy

git push -u origin release/0.13.4
gh pr create --base main --title "Flowlight 0.13.4"
# wait for review and every required PR check to pass
# after reviewing the final head, a login configured in RELEASE_REVIEWERS must post
# `Reviewed-Commit: <exact SHA>` as a PR comment; the PR author's editable body is not evidence
# wait for that authenticated receipt and every required PR check to pass

git tag -a v0.13.4 -m "Flowlight 0.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
git push origin v0.13.4 # prepares, attests and retains the only bytes publication may use
# inspect that successful preparation, then publish its exact artifact from the same tag:
gh workflow run Release --ref v0.13.4 -f prepare_run_id=<successful-tag-push-run-id>
# verify the GitHub release, DMG, PKG, SHA256SUMS.txt and RELEASE-MANIFEST.json before the final merge
gh pr merge --merge --delete-branch
```

**Tags are immutable.** Never retag, force-push or move a version after it has been pushed. If a tag is wrong or a
release needs another change, bump the patch version and begin a new release branch and tag.
**Release-tag creation is restricted and tags are immutable.** Only the designated release account may create a
`v*` tag. Never retag, force-push, or move a version after it has been pushed. If a tag is wrong or a release needs
another change, bump the patch version and begin a new release branch and tag.

**The PR precedes the tag, and the merge follows publication.** Review and green checks protect the exact tree that
will be signed. Publishing before merging keeps the GitHub Pages site from announcing a download that does not exist:
`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` 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`.
A tag push starts `release.yml` in preparation-only mode; push-triggered runs cannot publish. It verifies reviewed
exact-head evidence, builds/signs/notarizes once, passes Gatekeeper, records a manifest, attests every prepared file, and
retains one immutable Actions artifact. The explicit publication dispatch names that successful run, has no signing or
notarization credentials, and can only verify and publish those bytes. After it succeeds, verify the release page and
downloaded DMG, PKG, `SHA256SUMS.txt`, and `RELEASE-MANIFEST.json`, then merge the release PR into `main`.

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

Expand Down
12 changes: 6 additions & 6 deletions Flowlight.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -2252,7 +2252,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 77;
CURRENT_PROJECT_VERSION = 78;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES;
Expand All @@ -2267,7 +2267,7 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 0.17.3;
MARKETING_VERSION = 0.17.4;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
PRODUCT_NAME = "$(TARGET_NAME)";
Expand Down Expand Up @@ -2313,7 +2313,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 77;
CURRENT_PROJECT_VERSION = 78;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 38RJUJHKZS;
ENABLE_HARDENED_RUNTIME = YES;
Expand All @@ -2328,7 +2328,7 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 0.17.3;
MARKETING_VERSION = 0.17.4;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
PRODUCT_NAME = "$(TARGET_NAME)";
Expand Down Expand Up @@ -2393,7 +2393,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 77;
CURRENT_PROJECT_VERSION = 78;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES;
Expand All @@ -2414,7 +2414,7 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 0.17.3;
MARKETING_VERSION = 0.17.4;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
Expand Down
116 changes: 95 additions & 21 deletions Flowlight/App/AppNavigation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ enum SidebarItem: String, CaseIterable, Identifiable {
case .reports: return "worth-a-look"
case .visualize: return "visualize"
case .alerts: return "alerts"
case .rules: return "rules"
case .rules: return "policies"
case .ask: return "ask"
case .inspect: return "inspection"
case .sources: return "sources"
Expand All @@ -40,7 +40,7 @@ enum SidebarItem: String, CaseIterable, Identifiable {
case .reports: return "History"
case .visualize: return "Connections"
case .alerts: return "Alerts"
case .rules: return "Rules"
case .rules: return "Policies"
case .ask: return "Ask"
case .inspect: return "Inspect"
case .sources: return "Sources"
Expand All @@ -54,7 +54,7 @@ enum SidebarItem: String, CaseIterable, Identifiable {
case .reports: return L("History")
case .visualize: return L("Connections")
case .alerts: return L("Alerts")
case .rules: return L("Rules")
case .rules: return L("Policies")
case .ask: return L("Ask")
case .inspect: return L("Inspect")
case .sources: return L("Sources")
Expand Down Expand Up @@ -143,8 +143,8 @@ enum SidebarSection: String, CaseIterable, Identifiable {
}
}

/// The three operational views behind the single Sources destination. Keeping this in shared navigation lets a
/// status notice open the exact recovery surface without putting three configuration screens back in the sidebar.
/// The three sections in the single Sources destination. The cases remain navigation state so restored windows,
/// screenshot arguments and links can scroll to the exact operational section without hiding the other two.
enum SourcesTab: String, CaseIterable, Identifiable, Sendable {
case collector, devices, coverage

Expand All @@ -157,6 +157,19 @@ enum SourcesTab: String, CaseIterable, Identifiable, Sendable {
case .coverage: return L("Coverage")
}
}

var anchorID: String { "sources-\(rawValue)" }

/// `devices` and `coverage` were once standalone screens, then visible tabs. Both old forms still restore to
/// the corresponding section; an explicit modern section always wins over the legacy screen value.
static func restored(explicitRawValue: String?, legacyScreenRawValue: String?) -> SourcesTab {
if let explicitRawValue, let explicit = SourcesTab(rawValue: explicitRawValue) { return explicit }
switch legacyScreenRawValue {
case "devices": return .devices
case "coverage": return .coverage
default: return .collector
}
}
}

/// Alert review and its detection tuning share a destination, but app-wide alert links must still be able to select
Expand All @@ -168,20 +181,56 @@ enum AlertsTab: String, CaseIterable, Identifiable {
@MainActor var title: String { self == .alerts ? L("Alerts") : L("Detection") }
}

/// The three views inside Rules. This lives with the shared navigation state so an app-wide notice can take someone
/// straight to a changed pack, while ordinary rule links always land back on the rule list.
/// The two views inside Policies. The raw `rules` value is deliberately retained: restored windows and preferences
/// written before the product rename must keep landing on the inventory rather than silently resetting.
enum RulesTab: String, CaseIterable, Identifiable {
case rules, packs, activity
case rules, activity

var id: String { rawValue }

@MainActor var title: String {
switch self {
case .rules: return L("Rules")
case .packs: return L("Packs")
case .activity: return L("What they did")
case .rules: return L("Policies")
case .activity: return L("Activity")
}
}

/// `packs` was a visible tab through 0.17.3. It now restores to the unified inventory, where managed packs have
/// their own section. Unknown future values take the same conservative path.
static func restored(from rawValue: String?) -> RulesTab {
rawValue == RulesTab.activity.rawValue ? .activity : .rules
}

static func restoresManagedPackFocus(from rawValue: String?) -> Bool {
rawValue == "packs"
}
}

enum AgentPolicySection: Equatable, Sendable { case allowlist, guardrails }

struct AgentPolicyRequest: Equatable, Sendable {
var agentID: String
var section: AgentPolicySection
var id = UUID()
}

enum InspectPolicySection: Equatable, Sendable { case mocks, requestRewrites, responseTransforms }

/// A precise request into the policy inventory. It is intentionally source-shaped rather than carrying an editor:
/// each destination still owns its consent, validation, and save behavior.
enum PolicyDestination: Equatable, Sendable {
case rule(UUID)
case allowlist(String)
case managedPack(String?)
case guardrail(id: UUID, agentID: String)
case mock(UUID)
case requestRewrite(UUID)
case responseTransform(UUID)
}

struct PolicyRequest: Equatable, Sendable {
var destination: PolicyDestination
var id = UUID()
}

/// A rule the user started writing somewhere else — a table row, an alert — waiting for the Rules screen to open
Expand Down Expand Up @@ -215,22 +264,23 @@ final class AppNavigation: ObservableObject {
}()
@Published var sourcesTab: SourcesTab = {
let defaults = UserDefaults.standard
if let explicit = SourcesTab(rawValue: defaults.string(forKey: "FLSourcesTab") ?? "") {
return explicit
}
switch defaults.string(forKey: "FLScreen") {
case "devices": return .devices
case "coverage": return .coverage
default: return .collector
}
return SourcesTab.restored(explicitRawValue: defaults.string(forKey: "FLSourcesTab"),
legacyScreenRawValue: defaults.string(forKey: "FLScreen"))
}()
/// A separate pulse makes a repeated link to the already-selected section scroll again.
@Published private(set) var sourcesRequestID = UUID()
@Published var alertsTab: AlertsTab = .alerts
@Published var rulesTab: RulesTab = {
RulesTab(rawValue: UserDefaults.standard.string(forKey: "FLRulesTab") ?? "") ?? .rules
RulesTab.restored(from: UserDefaults.standard.string(forKey: "FLRulesTab"))
}()
@Published var reportRequest: ReportRequest?
@Published var inspectRequest: InspectRequest?
@Published var ruleRequest: RuleRequest?
@Published var policyRequest: PolicyRequest? = {
RulesTab.restoresManagedPackFocus(from: UserDefaults.standard.string(forKey: "FLRulesTab"))
? PolicyRequest(destination: .managedPack(nil)) : nil
}()
@Published var agentPolicyRequest: AgentPolicyRequest?

private struct TrailEntry {
var item: SidebarItem
Expand Down Expand Up @@ -289,12 +339,34 @@ final class AppNavigation: ObservableObject {

func showSources(_ tab: SourcesTab = .collector) {
sourcesTab = tab
sourcesRequestID = UUID()
show(.sources)
}

func showPackReviews() {
show(.rules)
rulesTab = .packs
policyRequest = PolicyRequest(destination: .managedPack(nil))
}

func showPolicyPack(_ packID: String) {
show(.rules)
policyRequest = PolicyRequest(destination: .managedPack(packID))
}

func showPolicy(_ destination: PolicyDestination) {
show(.rules)
policyRequest = PolicyRequest(destination: destination)
}

func showAgentPolicy(agentID: String, section: AgentPolicySection) {
agentPolicyRequest = AgentPolicyRequest(agentID: agentID, section: section)
followLink(to: .agents)
}

func showInspectionPolicy(_ section: InspectPolicySection) {
guard SidebarItem.availableCases.contains(.inspect) else { return }
inspectRequest = InspectRequest(search: "", policySection: section)
followLink(to: .inspect)
}

func showReport(filter: TrafficFilter, granularity: Granularity? = nil,
Expand Down Expand Up @@ -378,5 +450,7 @@ struct InspectRequest: Equatable {
var mcpServer: String? = nil
/// Explicitly displayed and removable in Inspect; Reports uses it for the potential-harm drill-in.
var potentialHarmOnly = false
/// Opens setup on the contextual editor that owns this kind of policy.
var policySection: InspectPolicySection? = nil
var id = UUID()
}
2 changes: 1 addition & 1 deletion Flowlight/Ask/AskProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ struct AskRecordedCall: Identifiable, Equatable, Sendable {
var chart: AskChart?
/// Which app screen clicking should open when Reports cannot represent this result.
var screen: SidebarItem?
/// The exact operational tab when that destination is Sources.
/// The exact section to focus when that destination is Sources.
var sourcesTab: SourcesTab? = nil
}

Expand Down
2 changes: 1 addition & 1 deletion Flowlight/Ask/AskQueryRunner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ enum AskQueryRunner {
var chart: AskChart?
/// Where the answer points for a question about the app rather than about traffic.
var screen: SidebarItem?
/// The exact operational tab when that screen is Sources.
/// The exact section to focus when that screen is Sources.
var sourcesTab: SourcesTab? = nil
}

Expand Down
Loading
Loading