-
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
Draft
seonghobae
wants to merge
49
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.
Draft
Changes from all commits
Commits
Show all changes
49 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 bb11a19
Merge remote-tracking branch 'origin/main' into fleet-incident-govern…
seonghobae c2a2430
fix(actions): bind lifecycle inventory to stable fleet evidence
seonghobae 2696165
experiment: revalidate owner issue evidence before publication
seonghobae 48abe0e
docs: require live proof before owner issue publication
seonghobae 19ff8c8
docs: define temporary workflow lifecycle contract
seonghobae e0c860c
feat: bind owner issue publication to reviewed ledger
seonghobae bf01048
fix: preserve lifecycle evidence and isolate owner updates
seonghobae 1c52305
test: repair Strix gate baseline fixtures
seonghobae 54033b9
test: assert REST workflow pagination requests
seonghobae 70a9dd3
merge: integrate Strix baseline fixture repair into issue 945
seonghobae e794828
merge: integrate CI foundation repairs into issue 945
seonghobae a75be4d
merge: integrate scoped Noema continuation repair into issue 945
seonghobae 354951e
merge: adopt deployed central self-hosted runner routing
seonghobae 01fd22f
Merge current main to repair protected review conflicts
seonghobae 2e6694d
fix(lifecycle): bind owner issue reuse to trusted authors
seonghobae 2441fe3
Merge canonical CodeQL runner contract repair
seonghobae 4423628
merge: adopt current central CI and trusted Strix fixtures
seonghobae 03bbd9b
merge: integrate canonical dispatch blob pin repair
seonghobae 2030cbd
Merge main into workflow lifecycle governance
seonghobae 15a62d0
fix(lifecycle): group orphan owner issues by repository
seonghobae 117a5b3
fix(lifecycle): reuse verified fleet owner issues
seonghobae 7e8106e
fix(actions): guard branch-owned workflow identities before retirement
seonghobae 6f645a7
test(lifecycle): cover owner-issue operator fail-closed paths
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
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
58 changes: 58 additions & 0 deletions
58
.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,58 @@ | ||
| 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" | ||
| - "scripts/ci/workflow_lifecycle_operator.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,scripts/ci/workflow_lifecycle_operator.py' --show-missing --fail-under=100 | ||
| python -m compileall -q scripts/ci/inventory_orphaned_workflows.py scripts/ci/workflow_lifecycle_operator.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,180 @@ | ||
| # 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. `orphan_active` means absent from the | ||
| bound default-branch tree; an open pull request can still use that path on | ||
| its head or base. It is not a safe-disable verdict. | ||
| 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. | ||
|
|
||
| ## Prevention contract for repository workflows | ||
|
|
||
| A bounded repair workflow must take one of two paths before its PR is merged: | ||
|
|
||
| 1. Keep the workflow on a short-lived branch, complete its work through a normal | ||
| PR, and remove the YAML before it reaches the protected default branch. It | ||
| never acquires a default-branch workflow registry identity. | ||
| 2. If the workflow must reach the protected default branch, record its lifecycle | ||
| owner and expiry condition in the PR. The owner must arrange a separately | ||
| reviewed, exact-ID registry-disable action after use, then retain the API | ||
| receipt and a fresh inventory showing the identity disabled. | ||
|
|
||
| Deleting the YAML alone does not satisfy the second path. A later inventory | ||
| finding remains open until the owner either completes that disablement or | ||
| documents an explicit reviewed exception. Neither a workflow name nor a | ||
| missing source file authorizes disablement by the read-only scanner. | ||
|
|
||
| ## 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 and owner-issue publisher expose no workflow-state mutation. | ||
| Disablement needs a separate reviewed operator path that rechecks the live | ||
| default-branch SHA, exact workflow ID, source absence, open pull request heads | ||
| and bases, owner intent, active runs, and reusable-workflow callers immediately | ||
| before the API write. 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. | ||
| The separately invoked owner-issue publisher verifies that each finding belongs | ||
| to the supplied ledger, then rechecks the live repository, default-branch SHA, | ||
| workflow ID/path/state, and complete source tree before posting. A changed | ||
| identity or restored source file stops publication. The publisher computes the | ||
| ledger's digest and scans existing owner issues before creating one for a | ||
| repository without a known route. It does not run in the read-only inventory | ||
| job. | ||
| For a repository without a known route, one trusted owner issue groups its | ||
| findings. Each revalidated workflow ID and path gets its own evidence comment. | ||
| Before posting, the publisher checks the open issue and every comment for the | ||
| exact evidence body, so retrying the same ledger does not duplicate a comment | ||
| or hide another workflow from that ledger. | ||
|
|
||
| For one separately reviewed finding, the operator supplies the exact ledger | ||
| digest, repository, and workflow ID: | ||
|
|
||
| ```bash | ||
| python3 -m scripts.ci.workflow_lifecycle_operator \ | ||
| --ledger /tmp/workflow-lifecycle-ledger.json \ | ||
| --expected-ledger-sha256 "$REVIEWED_LEDGER_SHA256" \ | ||
| --repository appguardrail --workflow-id "$REVIEWED_WORKFLOW_ID" | ||
| ``` | ||
|
|
||
| The digest binds the reviewed file; it is not approval or fleet-completeness | ||
| proof by itself. Review the ledger and its API receipts first. The command | ||
| accepts one identity, requires the canonical ledger and its completeness | ||
| marker, and reads live GitHub evidence again before its issue write. It never | ||
| disables a workflow. Existing issue markers are reused only for repository | ||
| owners, members, collaborators, or the established github-actions and | ||
| opencode-agent bot publishers. Other public issue authors cannot redirect | ||
| evidence or poison the duplicate-issue check. | ||
|
|
||
| ## Rollback | ||
|
|
||
| Rollback removes the inventory and owner-issue operator scripts, focused tests, | ||
| schema example, architecture entry, changelog entry, this doctoring record, | ||
| and both `.github/workflows/workflow-lifecycle-inventory.yml` and | ||
| `.github/workflows/workflow-lifecycle-inventory-quality-ci.yml` 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 |
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.