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
106 changes: 106 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
name: CI

on:
pull_request:
push:
branches:
- main

permissions:
contents: read

concurrency:
group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
macos-core:
name: macos-core
runs-on: macos-15
timeout-minutes: 30

steps:
- name: Check out source
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: "3.11"

- name: Set up Node.js
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: "20"
cache: npm
cache-dependency-path: apps/desktop/package-lock.json

- name: Install Python test dependencies
run: |
python -m venv .venv
.venv/bin/python -m pip install --disable-pip-version-check --no-input \
-e packages/drawio-exporter \
-e 'services/mining-core[dev]' \
-e 'services/local-api[dev]'

- name: Install locked desktop dependencies
run: npm --prefix apps/desktop ci --ignore-scripts

- name: Verify Rust lockfile
run: cargo check --locked --manifest-path apps/desktop/src-tauri/Cargo.toml

- name: Run repository quality gates
run: |
./scripts/test.sh
./scripts/lint.sh
./scripts/check_licenses.sh
./scripts/check_no_external_network.sh

macos-package-smoke:
name: macos-package-smoke
needs: macos-core
runs-on: macos-15
timeout-minutes: 30

steps:
- name: Check out source
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: "3.11"

- name: Set up Node.js
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: "20"
cache: npm
cache-dependency-path: apps/desktop/package-lock.json

- name: Install desktop dependencies
run: npm --prefix apps/desktop ci --ignore-scripts

- name: Build unsigned package smoke artifact
run: OPSMINEFLOW_SKIP_CHECKS=1 ./scripts/package_macos.sh

- name: Verify package checksums
run: |
shopt -s nullglob
artifacts=(dist/macos/OpsMineFlow.app.zip dist/macos/*.dmg)
if [[ ${#artifacts[@]} -ne 2 ]]; then
echo "Expected one app zip and one dmg artifact."
printf '%s\n' "${artifacts[@]}"
exit 1
fi
test -s dist/macos/SHA256SUMS.txt
for artifact in "${artifacts[@]}"; do
test -s "$artifact"
grep -F -- "$(basename "$artifact")" dist/macos/SHA256SUMS.txt >/dev/null
done
cd dist/macos
shasum -a 256 -c SHA256SUMS.txt
2 changes: 2 additions & 0 deletions README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

OpsMineFlow は、業務効率化コンサルティングの As-Is 調査、業務棚卸し、業務プロセス可視化、ボトルネック分析、自動化候補抽出、報告書ドラフト作成を支援する、Mac向けローカルファーストOSSだよ。

CIの状態、必須check、main保護の手順は [CI / main branch quality gate](docs/operations/CI.md) にまとめてる。

## プロダクト概要

商用SaaS契約なしで、CSV/JSONイベントログや任意のActivityWatchエクスポートをローカルに取り込み、業務フローと改善候補を可視化する。
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

OpsMineFlow is a local-first task mining and process mining assistant for macOS consultants. It helps with As-Is discovery, work inventory, process visualization, bottleneck analysis, automation candidate discovery, and consulting report drafts without requiring commercial SaaS contracts.

CI status, required checks, and the repository protection procedure are documented in [CI / main branch quality gate](docs/operations/CI.md).

## Why OpsMineFlow

Consulting teams often need to understand real work before proposing BPR, RPA, system renewal, or operating model changes. OpsMineFlow focuses on consent-based, local-only analysis that can be explained to client security, legal, and business teams.
Expand Down
38 changes: 38 additions & 0 deletions docs/operations/CI.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# CI / main branch quality gate

## Purpose

The `CI` workflow is the required macOS quality gate for every pull request and every push to `main`. It uses no repository secrets, defaults to read-only repository permissions, and does not publish artifacts, tags, releases, or telemetry.

The stable required check names are **`macos-core`** and **`macos-package-smoke`**. Keep those names stable when editing the workflow; GitHub main-branch protection references them exactly.

## What CI verifies

- Python 3.11 virtual-environment installation and Python test dependencies
- `npm ci` against `apps/desktop/package-lock.json`, which rejects Node dependency/lockfile drift
- `cargo check --locked` for the Tauri desktop shell
- Swift type checking through `./scripts/test.sh`
- `./scripts/test.sh`
- `./scripts/lint.sh`
- `./scripts/check_licenses.sh`
- `./scripts/check_no_external_network.sh`
- unsigned `.app`/`.dmg` package smoke plus `SHA256SUMS.txt` validation

The current workflow runs on GitHub's supported `macos-15` Apple Silicon runner. It proves that each artifact can be produced on that architecture; it does not establish an Intel support guarantee or a universal binary. #78 owns the v1 architecture policy and any Intel/universal release verification.

Python dependencies still use the project version constraints during this first CI slice. Hash-pinned Python dependency locking, SBOM, vulnerability scanning, and provenance are owned by #84; do not treat this workflow as satisfying those supply-chain controls.

## GitHub repository setting

After the workflow has run successfully on `main`, configure main-branch protection (or an equivalent repository ruleset) to:

1. Require a pull request before merging.
2. Require the `macos-core` and `macos-package-smoke` status checks and require them to be up to date.
3. Apply the rule to administrators as well as collaborators.
4. Do not allow force pushes or deletions of `main`.

The workflow itself deliberately does not hold permission to change protection. That configuration is a repository-admin action and is verified separately in issue #81.

## Fork and secret safety

The workflow runs on `pull_request`, not `pull_request_target`, and declares no secrets. Every third-party action is pinned to a full commit SHA. Keep both properties. A future notarization or release workflow must run only from a protected environment after explicit human approval; it must not share credentials with this workflow.
18 changes: 10 additions & 8 deletions scripts/check_licenses.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ echo "Checking license policy guardrails..."

FAILED=0
APACHE_2_0_SHA256="cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30"
MATCH_FILE="$(mktemp "${TMPDIR:-/tmp}/opsmineflow_license_match.XXXXXX")"
trap 'rm -f "$MATCH_FILE"' EXIT

if [[ ! -f LICENSE ]]; then
echo "LICENSE file is missing."
Expand Down Expand Up @@ -39,21 +41,21 @@ PROHIBITED_PACKAGES=(
segment
)

while IFS= read -r manifest; do
while IFS= read -r -d '' manifest; do
for package in "${PROHIBITED_PACKAGES[@]}"; do
if rg -n -i "(^|[\"' =_-])${package}([\"' <>=_-]|$)" "$manifest" >/tmp/opsmineflow_license_match.txt 2>/dev/null; then
if grep -E -n -i -- "(^|[\"' =_-])${package}([\"' <>=_-]|$)" "$manifest" >"$MATCH_FILE" 2>/dev/null; then
echo "Prohibited dependency candidate '$package' found in $manifest"
cat /tmp/opsmineflow_license_match.txt
cat "$MATCH_FILE"
FAILED=1
fi
done
done < <(find . -type f '(' -name 'package.json' -o -name 'pyproject.toml' -o -name 'requirements*.txt' -o -name 'Cargo.toml' ')' \
-not -path './node_modules/*' \
-not -path './.venv/*' \
-not -path './venv/*' \
-not -path './apps/desktop/src-tauri/target/*' | sort)
-not -path '*/node_modules/*' \
-not -path '*/.venv/*' \
-not -path '*/venv/*' \
-not -path './apps/desktop/src-tauri/target/*' -print0)

if ! rg -n "Apache-2.0" LICENSE README.md README.ja.md docs/licenses >/dev/null; then
if ! grep -R -n -I -- "Apache-2.0" LICENSE README.md README.ja.md docs/licenses >/dev/null; then
echo "Apache-2.0 license declaration was not found."
FAILED=1
fi
Expand Down
65 changes: 40 additions & 25 deletions scripts/check_no_external_network.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,31 @@ is_scan_metadata() {
|| "$file" == *"package-lock.json" ]]
}

while IFS= read -r match; do
file="${match%%:*}"
rest="${match#*:}"
value="${rest#*:}"
if [[ "$file" == "scripts/check_no_external_network.sh" || "$file" == "scripts/check_licenses.sh" || "$file" == "scripts/bootstrap_mac.sh" ]] || is_scan_metadata "$file"; then
continue
fi
if [[ "$value" =~ ^https?://(127\.0\.0\.1|localhost)([:/].*)?$ ]]; then
continue
fi
echo "External URL candidate: $file -> $value"
FAILED=1
done < <(rg -n -o "$URL_PATTERN" "${SCAN_PATHS[@]}" 2>/dev/null || true)
source_files() {
find "$@" -type f \
-not -path '*/node_modules/*' \
-not -path '*/.venv/*' \
-not -path '*/venv/*' \
-not -path '*/.pytest_cache/*' \
-not -path '*/dist/*' \
-not -path '*/src-tauri/target/*' \
-not -path '*/src-tauri/gen/*' \
-not -path '*/src-tauri/icons/*' -print0
}

while IFS= read -r -d '' file; do
while IFS= read -r match; do
value="${match#*:}"
if [[ "$file" == "scripts/check_no_external_network.sh" || "$file" == "scripts/check_licenses.sh" || "$file" == "scripts/bootstrap_mac.sh" ]] || is_scan_metadata "$file"; then
continue
fi
if [[ "$value" =~ ^https?://(127\.0\.0\.1|localhost)([:/].*)?$ ]]; then
continue
fi
echo "External URL candidate: $file -> $value"
FAILED=1
done < <(grep -E -n -I -o -- "$URL_PATTERN" "$file" 2>/dev/null || true)
done < <(source_files "${SCAN_PATHS[@]}")

PROHIBITED_TERMS=(
telemetry
Expand All @@ -49,20 +61,23 @@ PROHIBITED_TERMS=(
)

for term in "${PROHIBITED_TERMS[@]}"; do
while IFS= read -r match; do
file="${match%%:*}"
if [[ "$file" == "scripts/check_no_external_network.sh" || "$file" == "scripts/check_licenses.sh" || "$file" == "scripts/bootstrap_mac.sh" ]] || is_scan_metadata "$file"; then
continue
fi
echo "Prohibited integration term '$term' found in $match"
FAILED=1
done < <(rg -n -i "$term" apps services packages scripts 2>/dev/null || true)
while IFS= read -r -d '' file; do
while IFS= read -r match; do
if [[ "$file" == "scripts/check_no_external_network.sh" || "$file" == "scripts/check_licenses.sh" || "$file" == "scripts/bootstrap_mac.sh" ]] || is_scan_metadata "$file"; then
continue
fi
echo "Prohibited integration term '$term' found in $file:$match"
FAILED=1
done < <(grep -E -n -I -i -- "$term" "$file" 2>/dev/null || true)
done < <(source_files "${SCAN_PATHS[@]}")
done

if rg -n '"dangerousRemoteDomainIpcAccess"|externalBin|allowlist.*all|0\.0\.0\.0' apps packages services 2>/dev/null; then
echo "Potential unsafe network or Tauri configuration found."
FAILED=1
fi
while IFS= read -r -d '' file; do
if grep -E -n -I -- '"dangerousRemoteDomainIpcAccess"|externalBin|allowlist.*all|0\.0\.0\.0' "$file" 2>/dev/null; then
echo "Potential unsafe network or Tauri configuration found in $file."
FAILED=1
fi
done < <(source_files apps packages services)

if [[ "$FAILED" -ne 0 ]]; then
echo "Local-only network policy check failed."
Expand Down
12 changes: 10 additions & 2 deletions services/local-api/tests/test_api_logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,8 +294,16 @@ def test_diagnostics_exposes_storage_and_local_only_policy(self) -> None:
def test_diagnostic_checks_run_local_guardrails(self) -> None:
results = run_diagnostic_checks()

self.assertEqual(results["license_policy"]["status"], "passed")
self.assertEqual(results["local_network_policy"]["status"], "passed")
self.assertEqual(
results["license_policy"]["status"],
"passed",
results["license_policy"].get("output", ""),
)
self.assertEqual(
results["local_network_policy"]["status"],
"passed",
results["local_network_policy"].get("output", ""),
)

def test_import_preview_and_store_import_history(self) -> None:
preview = create_import_preview("csv", "data/sample/sample_events.csv")
Expand Down