From 8da727901fdca41e72dbc257907586dcddf55375 Mon Sep 17 00:00:00 2001 From: tuzuminami Date: Mon, 20 Jul 2026 07:59:04 +0900 Subject: [PATCH 1/6] ci: add macOS quality gates and package smoke --- .github/workflows/ci.yml | 106 +++++++++++++++++++++++++++++++++++++++ README.ja.md | 2 + README.md | 2 + docs/operations/CI.md | 38 ++++++++++++++ 4 files changed, 148 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 docs/operations/CI.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b25f62e --- /dev/null +++ b/.github/workflows/ci.yml @@ -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@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + 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@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + 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 diff --git a/README.ja.md b/README.ja.md index fa64a85..fadb7a4 100644 --- a/README.ja.md +++ b/README.ja.md @@ -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エクスポートをローカルに取り込み、業務フローと改善候補を可視化する。 diff --git a/README.md b/README.md index f206acf..eec4221 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/docs/operations/CI.md b/docs/operations/CI.md new file mode 100644 index 0000000..476fd84 --- /dev/null +++ b/docs/operations/CI.md @@ -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. From a4ab66793eb4f1b4e7dcd2796a47ada049b002fc Mon Sep 17 00:00:00 2001 From: tuzuminami Date: Mon, 20 Jul 2026 08:04:16 +0900 Subject: [PATCH 2/6] fix(ci): exclude vendored manifests from license guard --- scripts/check_licenses.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/check_licenses.sh b/scripts/check_licenses.sh index 4fd514b..3d9aa1e 100755 --- a/scripts/check_licenses.sh +++ b/scripts/check_licenses.sh @@ -48,9 +48,9 @@ while IFS= read -r manifest; do 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 '*/node_modules/*' \ + -not -path '*/.venv/*' \ + -not -path '*/venv/*' \ -not -path './apps/desktop/src-tauri/target/*' | sort) if ! rg -n "Apache-2.0" LICENSE README.md README.ja.md docs/licenses >/dev/null; then From f0eaa7fa20ecd3ed872908da3e7305fe309fad03 Mon Sep 17 00:00:00 2001 From: tuzuminami Date: Mon, 20 Jul 2026 08:07:20 +0900 Subject: [PATCH 3/6] test(ci): report diagnostic guardrail failures --- services/local-api/tests/test_api_logic.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/services/local-api/tests/test_api_logic.py b/services/local-api/tests/test_api_logic.py index 4cd40ec..00e3fac 100644 --- a/services/local-api/tests/test_api_logic.py +++ b/services/local-api/tests/test_api_logic.py @@ -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") From 00fb7cce1d89fc8fd6906a65eee39b97c600021e Mon Sep 17 00:00:00 2001 From: tuzuminami Date: Mon, 20 Jul 2026 08:10:25 +0900 Subject: [PATCH 4/6] fix(quality): remove ripgrep requirement from guardrails --- scripts/check_licenses.sh | 8 ++-- scripts/check_no_external_network.sh | 65 +++++++++++++++++----------- 2 files changed, 45 insertions(+), 28 deletions(-) diff --git a/scripts/check_licenses.sh b/scripts/check_licenses.sh index 3d9aa1e..7dfb670 100755 --- a/scripts/check_licenses.sh +++ b/scripts/check_licenses.sh @@ -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." @@ -41,9 +43,9 @@ PROHIBITED_PACKAGES=( while IFS= read -r 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 @@ -53,7 +55,7 @@ done < <(find . -type f '(' -name 'package.json' -o -name 'pyproject.toml' -o -n -not -path '*/venv/*' \ -not -path './apps/desktop/src-tauri/target/*' | sort) -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 diff --git a/scripts/check_no_external_network.sh b/scripts/check_no_external_network.sh index 25e9afa..717db72 100755 --- a/scripts/check_no_external_network.sh +++ b/scripts/check_no_external_network.sh @@ -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/*' | sort +} + +while IFS= read -r 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 @@ -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 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 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." From fa6c19ad4e09d04f58c225673dcb731ef59cf323 Mon Sep 17 00:00:00 2001 From: tuzuminami Date: Mon, 20 Jul 2026 08:12:36 +0900 Subject: [PATCH 5/6] fix(quality): handle unusual filenames in guardrails --- scripts/check_licenses.sh | 4 ++-- scripts/check_no_external_network.sh | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/scripts/check_licenses.sh b/scripts/check_licenses.sh index 7dfb670..ac39c05 100755 --- a/scripts/check_licenses.sh +++ b/scripts/check_licenses.sh @@ -41,7 +41,7 @@ PROHIBITED_PACKAGES=( segment ) -while IFS= read -r manifest; do +while IFS= read -r -d '' manifest; do for package in "${PROHIBITED_PACKAGES[@]}"; do if grep -E -n -i -- "(^|[\"' =_-])${package}([\"' <>=_-]|$)" "$manifest" >"$MATCH_FILE" 2>/dev/null; then echo "Prohibited dependency candidate '$package' found in $manifest" @@ -53,7 +53,7 @@ done < <(find . -type f '(' -name 'package.json' -o -name 'pyproject.toml' -o -n -not -path '*/node_modules/*' \ -not -path '*/.venv/*' \ -not -path '*/venv/*' \ - -not -path './apps/desktop/src-tauri/target/*' | sort) + -not -path './apps/desktop/src-tauri/target/*' -print0) 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." diff --git a/scripts/check_no_external_network.sh b/scripts/check_no_external_network.sh index 717db72..b5bf9bb 100755 --- a/scripts/check_no_external_network.sh +++ b/scripts/check_no_external_network.sh @@ -28,10 +28,10 @@ source_files() { -not -path '*/dist/*' \ -not -path '*/src-tauri/target/*' \ -not -path '*/src-tauri/gen/*' \ - -not -path '*/src-tauri/icons/*' | sort + -not -path '*/src-tauri/icons/*' -print0 } -while IFS= read -r file; do +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 @@ -61,7 +61,7 @@ PROHIBITED_TERMS=( ) for term in "${PROHIBITED_TERMS[@]}"; do - while IFS= read -r file; do + 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 @@ -72,7 +72,7 @@ for term in "${PROHIBITED_TERMS[@]}"; do done < <(source_files "${SCAN_PATHS[@]}") done -while IFS= read -r file; do +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 From cac33903e94e4be3bafa55feb1880cb989fbc0bf Mon Sep 17 00:00:00 2001 From: tuzuminami Date: Mon, 20 Jul 2026 08:20:48 +0900 Subject: [PATCH 6/6] ci: update actions to Node 24 runtime --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b25f62e..50c7e89 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,12 +21,12 @@ jobs: steps: - name: Check out source - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 with: persist-credentials: false - name: Set up Python - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: python-version: "3.11" @@ -66,12 +66,12 @@ jobs: steps: - name: Check out source - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 with: persist-credentials: false - name: Set up Python - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: python-version: "3.11"