-
Notifications
You must be signed in to change notification settings - Fork 0
feat(actions): inventory orphaned workflow identities #1026
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
seonghobae
wants to merge
26
commits into
main
Choose a base branch
from
feat/orphaned-workflow-lifecycle-inventory
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+2,532
−0
Open
Changes from all commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
bbd7fa9
feat(actions): inventory orphaned workflow identities
seonghobae 1e84d65
Merge branch 'main' into feat/orphaned-workflow-lifecycle-inventory
opencode-agent[bot] e7a6cab
Merge branch 'main' into feat/orphaned-workflow-lifecycle-inventory
opencode-agent[bot] 8d08bac
Merge branch 'main' into feat/orphaned-workflow-lifecycle-inventory
opencode-agent[bot] 271ad3a
docs: keep workflow caller note with repair gate
seonghobae ab51f48
fix: classify workflow ledger write failures
seonghobae e281305
fix(actions): route fleet orphan findings to owners
seonghobae 580c8e9
style(actions): lint lifecycle inventory
seonghobae 30644f0
Merge remote-tracking branch 'refs/remotes/origin/main' into HEAD
seonghobae 442ba6e
fix: complete orphan workflow owner routing
seonghobae 8d141d5
docs: clarify workflow path validation
seonghobae 84b84ae
fix(actions): bind workflow inventory to consumed pages
seonghobae ef77ced
Merge remote-tracking branch 'origin/main' into HEAD
seonghobae 71c0cc8
fix: fail closed on partial workflow inventories
seonghobae 3710efc
Merge branch 'main' into feat/orphaned-workflow-lifecycle-inventory
opencode-agent[bot] d70388a
Merge branch 'main' into feat/orphaned-workflow-lifecycle-inventory
opencode-agent[bot] 740379a
test: align scheduler contract and audit runtime
seonghobae 1be7698
Merge protected main into orphaned workflow inventory
seonghobae f1344a8
Merge remote-tracking branch 'origin/main' into HEAD
seonghobae 8e24b8b
fix: collect live workflow lifecycle evidence
seonghobae 9edc4e1
test: enforce workflow lifecycle prevention contract
seonghobae 5b2b495
Merge remote-tracking branch 'origin/main' into HEAD
seonghobae 63f4e43
test: close scheduler coverage gaps
seonghobae 4aedb7e
fix(inventory): prove complete live fleet evidence
seonghobae 2cbd370
Merge remote-tracking branch 'origin/main' into merge-main-into-1026
claude d17ff23
merge: main@f2f91b80 into #1026 (feat/orphaned-workflow-lifecycle-inv…
seonghobae File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
57 changes: 57 additions & 0 deletions
57
.github/workflows/workflow-lifecycle-inventory-quality-ci.yml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| name: Workflow Lifecycle Inventory Quality CI | ||
|
|
||
| on: | ||
| pull_request: | ||
| branches: [main] | ||
| paths: | ||
| - ".github/workflows/workflow-lifecycle-inventory.yml" | ||
| - ".github/workflows/workflow-lifecycle-inventory-quality-ci.yml" | ||
| - "scripts/ci/inventory_orphaned_workflows.py" | ||
| - "tests/test_inventory_orphaned_workflows.py" | ||
| - "tests/test_workflow_lifecycle_inventory_workflow.py" | ||
| - "docs/doctoring/orphaned-workflow-lifecycle.md" | ||
| - "ARCHITECTURE.md" | ||
| - "CHANGELOG.md" | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| exact-head-quality: | ||
| runs-on: ubuntu-24.04 | ||
| timeout-minutes: 10 | ||
| steps: | ||
| - name: Checkout exact pull-request head | ||
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | ||
| with: | ||
| ref: ${{ github.event.pull_request.head.sha }} | ||
| persist-credentials: false | ||
|
|
||
| - name: Set up Python | ||
| uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | ||
| with: | ||
| python-version: "3.14" | ||
|
|
||
| - name: Install hash-verified test tools | ||
| shell: bash --noprofile --norc -e -o pipefail {0} | ||
| run: | | ||
| python -m pip install --only-binary=:all: --require-hashes -r <(cat <<'EOF' | ||
| coverage==7.15.2 --hash=sha256:b9a6367e4aff723e8ee8190836836124284e8fcd4265e307c844010cfa074f3f | ||
| iniconfig==2.1.0 --hash=sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760 | ||
| packaging==26.2 --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e | ||
| pluggy==1.6.0 --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 | ||
| pygments==2.20.0 --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 | ||
| pytest==9.1.1 --hash=sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c | ||
| EOF | ||
| ) | ||
|
|
||
| - name: Prove exact-head behavior and branch coverage | ||
| shell: bash --noprofile --norc -e -o pipefail {0} | ||
| run: | | ||
| test "$(git rev-parse HEAD)" = "${{ github.event.pull_request.head.sha }}" | ||
| coverage run --branch -m pytest --import-mode=importlib \ | ||
| tests/test_inventory_orphaned_workflows.py \ | ||
| tests/test_workflow_lifecycle_inventory_workflow.py -q | ||
| coverage report --include='scripts/ci/inventory_orphaned_workflows.py' --show-missing --fail-under=100 | ||
| python -m compileall -q scripts/ci/inventory_orphaned_workflows.py tests/test_inventory_orphaned_workflows.py | ||
| git diff --exit-code |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| name: Workflow Lifecycle Inventory | ||
|
|
||
| on: | ||
| schedule: | ||
| - cron: "31 4 * * *" | ||
|
|
||
| concurrency: | ||
| group: workflow-lifecycle-inventory | ||
| cancel-in-progress: false | ||
|
|
||
| permissions: | ||
| contents: read | ||
| actions: read | ||
|
|
||
| jobs: | ||
| inventory: | ||
| if: >- | ||
| github.repository == 'ContextualWisdomLab/.github' && | ||
| github.ref == format('refs/heads/{0}', github.event.repository.default_branch) | ||
| runs-on: ubuntu-24.04 | ||
| timeout-minutes: 30 | ||
| env: | ||
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" | ||
| steps: | ||
| - name: Harden runner | ||
| uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.13.2 | ||
| with: | ||
| egress-policy: block | ||
| allowed-endpoints: >- | ||
| api.github.com:443 | ||
| github.com:443 | ||
| objects.githubusercontent.com:443 | ||
| release-assets.githubusercontent.com:443 | ||
| results-receiver.actions.githubusercontent.com:443 | ||
| *.actions.githubusercontent.com:443 | ||
| *.blob.core.windows.net:443 | ||
|
|
||
| - name: Checkout exact trusted inventory source | ||
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | ||
| with: | ||
| ref: ${{ github.sha }} | ||
| persist-credentials: false | ||
|
|
||
| - name: Set up Python | ||
| uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | ||
| with: | ||
| python-version: "3.14" | ||
|
|
||
| - name: Collect read-only organization evidence | ||
| env: | ||
| GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }} | ||
| shell: bash --noprofile --norc -e -o pipefail {0} | ||
| run: | | ||
| if [ -z "${GH_TOKEN:-}" ]; then | ||
| echo "::error::PR_REVIEW_MERGE_TOKEN is required for complete organization visibility." | ||
| exit 1 | ||
| fi | ||
| echo "::add-mask::$GH_TOKEN" | ||
| test "$(git rev-parse HEAD)" = "$GITHUB_SHA" | ||
| python scripts/ci/inventory_orphaned_workflows.py --live \ | ||
| --output "$RUNNER_TEMP/workflow-lifecycle-ledger.json" \ | ||
| --receipt-output "$RUNNER_TEMP/workflow-lifecycle-api-receipts.json" \ | ||
| --failure-output "$RUNNER_TEMP/workflow-lifecycle-failure.json" | ||
| python -m json.tool "$RUNNER_TEMP/workflow-lifecycle-ledger.json" >/dev/null | ||
| python -m json.tool "$RUNNER_TEMP/workflow-lifecycle-api-receipts.json" >/dev/null | ||
|
|
||
| - name: Preserve immutable read-only evidence | ||
| if: always() | ||
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | ||
| with: | ||
| name: workflow-lifecycle-inventory-${{ github.run_id }}-${{ github.run_attempt }} | ||
| path: | | ||
| ${{ runner.temp }}/workflow-lifecycle-ledger.json | ||
| ${{ runner.temp }}/workflow-lifecycle-api-receipts.json | ||
| ${{ runner.temp }}/workflow-lifecycle-failure.json | ||
| if-no-files-found: error | ||
| retention-days: 30 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| # Orphaned GitHub Actions workflow-lifecycle inventory | ||
|
|
||
| 검토 기준일: **2026-08-16** | ||
|
|
||
| ## Incident | ||
|
|
||
| Live Actions inventories showed the same recurrence in multiple | ||
| ContextualWisdomLab repositories (ContextualWisdomLab/.github#945): | ||
|
|
||
| - AppGuardrail advertised dozens of historical `apply-*`, `finalize-*`, | ||
| and `*-once.yml` identities as `state: active` while sampled default-branch | ||
| paths returned 404 (ContextualWisdomLab/appguardrail#929); | ||
| - Clearfolio retained `one-shot-*` and PR-specific repair identities after | ||
| the YAML had left the protected default branch (ContextualWisdomLab/clearfolio#423); | ||
| - DiskSage retained PR-specific finalizers in the same shape | ||
| (ContextualWisdomLab/disksage#191). | ||
|
|
||
| Source deletion is not a complete workflow lifecycle. GitHub persists | ||
| registry records independently of the default-branch tree, so a buyer or | ||
| reviewer cannot treat "the YAML is gone" as "no writer remains enabled." | ||
|
|
||
| ## Decision | ||
|
|
||
| 1. The central `.github` repository owns a **read-only** inventory that | ||
| binds every advertised workflow identity to the exact protected | ||
| default-branch SHA observed at the start and re-read at the end. | ||
| 2. Classification is evidence-based: `present_active`, `present_disabled`, | ||
| `orphan_active`, `orphan_disabled`, `dynamic_owned`, or `unresolved`. | ||
| A file named `once` is not alone proof of invalidity. A benign name | ||
| does not hide a missing source file. | ||
| 3. Incomplete visibility (401/403/404), a 5xx after one retry, pagination | ||
| truncation, `total_count` drift, reused workflow IDs, percent-encoded | ||
| paths, and default-branch movement fail closed. | ||
| 4. This scanner never disables, deletes, or recreates workflows. Disablement | ||
| remains a separately reviewed operator step after the ledger is | ||
| revalidated. | ||
| 5. `NVIDIA_NIM_API_KEY` may exist elsewhere in the control plane. This | ||
| inventory never reads `COPILOT_GITHUB_TOKEN`. | ||
| 6. CSAP and SOC 2 are design constraints (access visibility, change | ||
| management, evidence retention). This record is not a certification | ||
| claim. Operational identities (repository, workflow path, workflow ID) | ||
| are not masked as PII. | ||
| 7. Confirmed repository owner routes are maintained as an explicit, | ||
| linkable registry from live fleet evidence. Repository slugs are matched | ||
| case-insensitively, and both `orphan_active` and `orphan_disabled` | ||
| classifications retain the route. The scanner does not infer issue | ||
| numbers, create issues, or convert an absent owner route into a passing | ||
| result. | ||
|
|
||
| ## Trust boundary | ||
|
|
||
| The production CLI uses `--live` with the established central `GH_TOKEN` | ||
| transport. It paginates all visible repositories and workflows, rejects a | ||
| truncated recursive tree, and re-reads each default-branch head. A mandatory | ||
| API receipt file content-binds every read. Fixture input remains available for | ||
| deterministic tests. Neither boundary receives `secrets: inherit` or a guessed PAT. | ||
| GitHub-owned `dynamic/` identities are never treated as deleted repository | ||
| files. | ||
|
|
||
| The ledger improves operational visibility into enabled control-plane writers; | ||
| that visibility gap is not itself CWE-200 sensitive-information exposure. | ||
| CWE-862 describes missing authorization when a registry mutation is performed | ||
| without a reviewed operator path. This increment closes the visibility gap and | ||
| refuses the mutation. | ||
|
|
||
| ## Operator contract | ||
|
|
||
| For a live read-only sweep, run: | ||
|
|
||
| ```bash | ||
| python3 scripts/ci/inventory_orphaned_workflows.py --live \ | ||
| --output /tmp/workflow-lifecycle-ledger.json \ | ||
| --receipt-output /tmp/workflow-lifecycle-api-receipts.json \ | ||
| --failure-output /tmp/workflow-lifecycle-failure.json | ||
| ``` | ||
|
|
||
| The protected-default-branch integration is | ||
| `.github/workflows/workflow-lifecycle-inventory.yml`. Its scheduled runs have | ||
| read-only repository permissions, verify the checked-out SHA, and | ||
| retain completed API receipts plus either the immutable ledger or structured | ||
| failure evidence for 30 days. The live collector proves fleet completeness by | ||
| matching the paginated repository list to authenticated organization-wide | ||
| public/private totals; pagination alone is not accepted. It contains no disable endpoint; | ||
| operator mutation remains a later reviewed action. | ||
|
|
||
| For fixture verification, feed a JSON payload with `organization`, `observed_at`, | ||
| `repository_inventory_complete: true`, and one object per visible non-archived | ||
| repository. The completeness flag is mandatory: a partial repository list must | ||
| fail closed instead of producing a ledger that overstates fleet coverage. Each | ||
| repository must include the | ||
| start and end default-branch SHAs, the exact tree paths at that SHA, and | ||
| complete workflow pages (`total_count`, `workflows`, and either `_link_next` | ||
| or a GitHub `Link` header). Archived repositories are skipped. | ||
|
|
||
| ```bash | ||
| python3 scripts/ci/inventory_orphaned_workflows.py \ | ||
| --payload schemas/examples/cwl-workflow-lifecycle-ledger-v1.example.json \ | ||
| --output /tmp/workflow-lifecycle-ledger.json | ||
| ``` | ||
|
|
||
| The scanner never exports a write primitive. The separately reviewed | ||
| `scripts/ci/workflow_lifecycle_operator.py` module's | ||
| `disable_confirmed_orphan` primitive accepts only an immutable `orphan_active` record and an identical | ||
| fresh head SHA, then addresses its exact numeric workflow ID. After a reviewed | ||
| operator pass, rerun the organization sweep and retain both receipt sets. | ||
| Known AppGuardrail, Clearfolio, and DiskSage owner routes bind the same live | ||
| evidence to their governance issues without heuristic issue creation. | ||
|
|
||
| ## Rollback | ||
|
|
||
| Rollback removes the inventory script, focused tests, schema example, | ||
| architecture entry, changelog entry, and this doctoring record together. No | ||
| registry state is mutated, so rollback does not re-enable or disable workflows. | ||
|
|
||
| ## References | ||
|
|
||
| GitHub. (2026). *REST API endpoints for workflows*. GitHub Docs. | ||
| https://docs.github.com/en/rest/actions/workflows | ||
|
|
||
| GitHub. (2026). *Security hardening for GitHub Actions*. GitHub Docs. | ||
| https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions | ||
|
|
||
| MITRE. (2026a). *CWE-200: Exposure of sensitive information to an unauthorized actor*. | ||
| https://cwe.mitre.org/data/definitions/200.html | ||
|
|
||
| MITRE. (2026b). *CWE-862: Missing authorization*. | ||
| https://cwe.mitre.org/data/definitions/862.html |
41 changes: 41 additions & 0 deletions
41
schemas/examples/cwl-workflow-lifecycle-ledger-v1.example.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| { | ||
| "organization": "ContextualWisdomLab", | ||
| "observed_at": "2026-08-16T12:00:00Z", | ||
| "repository_inventory_complete": true, | ||
| "repositories": [ | ||
| { | ||
| "name": "appguardrail", | ||
| "archived": false, | ||
| "default_branch": "main", | ||
| "default_branch_sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", | ||
| "default_branch_sha_after": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", | ||
| "tree_paths": [".github/workflows/ci.yml"], | ||
| "workflow_pages": [ | ||
| { | ||
| "total_count": 3, | ||
| "workflows": [ | ||
| { | ||
| "id": 11, | ||
| "name": "finalize-once", | ||
| "path": ".github/workflows/finalize-once.yml", | ||
| "state": "active" | ||
| }, | ||
| { | ||
| "id": 12, | ||
| "name": "ci", | ||
| "path": ".github/workflows/ci.yml", | ||
| "state": "active" | ||
| }, | ||
| { | ||
| "id": 13, | ||
| "name": "pages", | ||
| "path": "dynamic/pages/pages-build-deployment", | ||
| "state": "active" | ||
| } | ||
| ], | ||
| "_link_next": false | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.