fix(xbrl): statement assembly dropped two kinds of fact metadata, in six duplicated blocks #2908
Workflow file for this run
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 workflow installs Python dependencies and runs tests across versions. | |
| # | |
| # Cost design (to stay within GitHub Actions included minutes): | |
| # - Pull requests: fast tests only, single Python version (cheap, quick gate) | |
| # - Push to main: fast (full matrix) + network + slow (real integration gate) | |
| # - Nightly (cron): full suite (freshness vs SEC API) | |
| # - concurrency: cancel superseded PR runs; main runs always complete | |
| # - pip cache: avoid reinstalling deps from scratch in every job | |
| # | |
| # There is deliberately NO paths-ignore. A docs-only change would then report no | |
| # checks at all, and a required status check that never reports blocks the pull | |
| # request forever. Since these checks are meant to become required, every PR has | |
| # to produce them — the few CI minutes a markdown-only run costs are cheaper than | |
| # a merge queue that deadlocks on documentation. | |
| # | |
| # REQUIRED CHECKS ARE PINNED TO JOB NAMES. `main` requires exactly two contexts: | |
| # | |
| # Cassette safety gate | |
| # test-fast (3.13) | |
| # | |
| # The second name is generated from the matrix below, which is event-dependent — | |
| # a pull request builds only 3.13, so the context is literally "test-fast (3.13)". | |
| # Renaming a job, or changing the pull-request leg of that matrix, retires the | |
| # context that branch protection is waiting for; it never reports, and every pull | |
| # request blocks forever with enforce_admins on. If you touch either, update the | |
| # protection contexts in the same commit: | |
| # | |
| # gh api repos/dgunning/edgartools/branches/main/protection \ | |
| # --jq .required_status_checks.contexts | |
| # | |
| # test-strict-errors, test-network, test-slow and Combine Coverage are | |
| # deliberately NOT required: they carry `if: github.event_name != 'pull_request'` | |
| # and so report as skipped on every pull request. They run on push to main, | |
| # which is the integration gate. | |
| # | |
| # Because they cannot gate, they are REPORTED instead — see the `report` job at | |
| # the end of this file. Combine Coverage is where the 65% floor is enforced | |
| # (`coverage report --fail-under=65`), and until 2026-08-10 a failure there, or | |
| # in test-network or test-slow, appeared in the Actions tab and nowhere else. | |
| # That is the same hole #999 closed for the regression workflow after a red main | |
| # survived three merges unnoticed (bead edgartools-hwdp); this workflow was left | |
| # out of that fix even though it carries more. | |
| name: Build and Test Edgartools | |
| on: | |
| push: | |
| branches: [ "main" ] | |
| pull_request: | |
| branches: [ "main" ] | |
| schedule: | |
| # Nightly full suite at 07:00 UTC | |
| - cron: '0 7 * * *' | |
| workflow_dispatch: | |
| # Cancel an in-progress run when a newer commit lands on the same PR. Pushes to | |
| # main are never cancelled: PRs merge without being up to date with main, so the | |
| # post-merge run is the only check that the combination works, and back-to-back | |
| # merges used to cancel it before it finished. | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: ${{ github.event_name != 'push' }} | |
| jobs: | |
| # Must pass before any job that runs pytest. vcrpy loads cassettes with | |
| # PyYAML's unsafe loader, so a cassette carrying a python/ tag executes code | |
| # in the runner the moment a test touches it — gating after the suite has | |
| # started would be no gate at all. Cheap (~10s) and needs only PyYAML. | |
| cassette-gate: | |
| name: Cassette safety gate | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Set up Python 3.13 | |
| uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 | |
| with: | |
| python-version: '3.13' | |
| - name: Install PyYAML | |
| run: python -m pip install --upgrade pip PyYAML | |
| - name: Scan cassettes for unsafe YAML tags | |
| run: python scripts/check_cassettes.py | |
| test-fast: | |
| # PRs: single version for a quick gate. Push/schedule: full matrix. | |
| needs: cassette-gate | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| python-version: ${{ github.event_name == 'pull_request' && fromJSON('["3.13"]') || fromJSON('["3.10", "3.13"]') }} | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Set up Python ${{ matrix.python-version }} | |
| uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| cache: 'pip' | |
| - name: Install dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| python -m pip install -e ".[ai]" | |
| python -m pip install pytest pytest-cov pytest-env pytest-xdist pytest-asyncio pytest-retry pytest-mock pytest-vcr "vcrpy<8.2" freezegun==1.5.1 filelock tqdm responses | |
| # A source scan, so it runs here rather than as its own job: this is the | |
| # only context pull requests are required to report, and adding a second | |
| # required one means updating branch protection in lockstep (see the header | |
| # of this file). It is also why the check reads source instead of hooking | |
| # pytest — half the regression tree is network-marked and runs post-merge, | |
| # so a runtime hook would not see those tests on a pull request at all. | |
| - name: Check no regression test skips itself | |
| run: python scripts/check_regression_skips.py | |
| # Same reasoning, different property: a regression test has to name the bug | |
| # it guards, or nobody can answer "is this still reachable?" and the tree can | |
| # never be pruned. Static like the check above, and for the same reason — it | |
| # has to see the post-merge half of the tree on a pull request. | |
| - name: Check every regression test names its bug | |
| run: python scripts/check_regression_provenance.py | |
| - name: Run fast tests | |
| run: | | |
| # Parallelize fast tests - safe, no SEC API calls. | |
| # | |
| # No `and not regression` here, unlike the network and slow jobs. The | |
| # regression tree is 1,746 tests and every one of them used to be | |
| # deselected on pull requests, so a PR could break any of them and merge | |
| # green (bead edgartools-07lk.21). conftest.py now classifies each as | |
| # fast or network by measurement, and the 934 offline ones gate here. | |
| # The network half still runs only in the Regression Tests workflow — | |
| # PRs must not hammer the SEC endpoint. | |
| pytest -n auto --cov --cov-report=xml -m 'fast' | |
| - name: Upload coverage data | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| if: matrix.python-version == '3.13' && github.event_name != 'pull_request' | |
| with: | |
| name: coverage-fast | |
| path: | | |
| .coverage | |
| coverage.xml | |
| include-hidden-files: true | |
| retention-days: 1 | |
| test-strict-errors: | |
| # Runs the offline suite the way 6.0 will behave, today. | |
| # | |
| # EDGARTOOLS_STRICT_ERRORS=1 turns on the error changes that are otherwise a | |
| # 6.0 break (bead edgartools-07lk.10): the network boundary wraps httpx | |
| # exceptions into TransportError, and — as PR3 lands them — the silent-None | |
| # returns raise instead. The point is not to test users' code; it is to find | |
| # OUR code that still relies on the old behaviour, while there is still a | |
| # 5.x release to fix it in. Without this job, the first time all the 6.0 | |
| # error paths run together is 6.0 itself. | |
| # | |
| # The design for this bead planned it as continue-on-error, on the | |
| # assumption it would arrive amber and go green as the flips landed. It | |
| # arrives GREEN — the whole fast suite passes under the flag as of PR2 — so | |
| # it is a hard failure from the start. An allowed-to-fail lane that is | |
| # already passing teaches nobody anything, and this repository has already | |
| # paid for the lesson that an unreported post-merge failure survives several | |
| # merges (bead edgartools-hwdp, #999). It is in the `report` job's `needs` | |
| # accordingly. | |
| # | |
| # Post-merge only, matching test-network. It cannot be a REQUIRED check | |
| # while it skips pull requests — see the deadlock described at the top of | |
| # this file — so it is reported rather than gating, and moves to the pull | |
| # request leg when the 6.0 branch is cut and strict becomes the only path. | |
| if: github.event_name != 'pull_request' | |
| needs: cassette-gate | |
| runs-on: ubuntu-latest | |
| env: | |
| EDGARTOOLS_STRICT_ERRORS: '1' | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Set up Python 3.13 | |
| uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 | |
| with: | |
| python-version: '3.13' | |
| cache: 'pip' | |
| - name: Install dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| python -m pip install -e ".[ai]" | |
| python -m pip install pytest pytest-cov pytest-env pytest-xdist pytest-asyncio pytest-retry pytest-mock pytest-vcr "vcrpy<8.2" freezegun==1.5.1 filelock tqdm responses | |
| - name: Run fast tests with 6.0 error behaviour | |
| run: | | |
| # Same selection as test-fast. Offline only: what this is looking for is | |
| # internal code that mishandles the wrapped exception types, and that | |
| # shows up in mocked and cassette-backed tests just as well as in live | |
| # ones — without a second pass over the SEC endpoint. | |
| pytest -n auto -m 'fast' | |
| test-network: | |
| # Network tests skip PRs — SEC API behavior doesn't change per-commit. | |
| # Runs on push to main, nightly schedule, and manual dispatch. | |
| if: github.event_name != 'pull_request' | |
| needs: cassette-gate | |
| runs-on: ubuntu-latest | |
| env: | |
| # Relocate the EDGAR data/cache dir into the workspace so it can be cached. | |
| # This only moves where the HTTP cache (_tcache) lives; it does NOT enable | |
| # local storage (that needs EDGAR_USE_LOCAL_DATA, which tests disable). | |
| EDGAR_LOCAL_DATA_DIR: ${{ github.workspace }}/.edgar-data | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Set up Python 3.12 | |
| uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 | |
| with: | |
| python-version: "3.12" | |
| cache: 'pip' | |
| # Persist the SEC HTTP cache across runs. Filing documents and XBRL under | |
| # /Archives/edgar/data are cached forever (see CACHE_RULES), so a warm cache | |
| # serves the bulk of the suite from disk instead of re-downloading from SEC. | |
| # run_id-suffixed key always writes a fresh entry; restore-keys warm-starts | |
| # from the most recent prior cache (monotonic, GH evicts LRU at the 10GB cap). | |
| - name: Restore SEC HTTP cache | |
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| with: | |
| path: ${{ github.workspace }}/.edgar-data/_tcache | |
| key: edgar-tcache-network-${{ github.run_id }} | |
| restore-keys: | | |
| edgar-tcache-network- | |
| - name: Install dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| python -m pip install -e ".[ai]" | |
| python -m pip install pytest pytest-cov pytest-env pytest-xdist pytest-asyncio pytest-retry pytest-mock pytest-vcr "vcrpy<8.2" freezegun==1.5.1 filelock tqdm responses | |
| - name: Run network tests | |
| run: | | |
| # Sequential - respects SEC rate limits. --enable-cache persists HTTP | |
| # responses to _tcache (immutable /Archives data cached forever). | |
| pytest --cov --cov-report=xml -m 'network and not slow and not regression' --enable-cache | |
| - name: Upload coverage data | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: coverage-network | |
| path: | | |
| .coverage | |
| coverage.xml | |
| include-hidden-files: true | |
| retention-days: 1 | |
| test-slow: | |
| # Slow tests skip PRs (often network-heavy). Push/schedule/dispatch only. | |
| if: github.event_name != 'pull_request' | |
| needs: cassette-gate | |
| runs-on: ubuntu-latest | |
| env: | |
| EDGAR_LOCAL_DATA_DIR: ${{ github.workspace }}/.edgar-data | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Set up Python 3.12 | |
| uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 | |
| with: | |
| python-version: "3.12" | |
| cache: 'pip' | |
| # Persist the SEC HTTP cache (see test-network for rationale). | |
| - name: Restore SEC HTTP cache | |
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| with: | |
| path: ${{ github.workspace }}/.edgar-data/_tcache | |
| key: edgar-tcache-slow-${{ github.run_id }} | |
| restore-keys: | | |
| edgar-tcache-slow- | |
| - name: Install dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| python -m pip install -e ".[ai]" | |
| python -m pip install pytest pytest-cov pytest-env pytest-xdist pytest-asyncio pytest-retry pytest-mock pytest-vcr "vcrpy<8.2" freezegun==1.5.1 filelock tqdm responses | |
| - name: Run slow tests | |
| run: | | |
| # Sequential - often network-heavy. --enable-cache persists HTTP responses. | |
| pytest --cov --cov-report=xml -m 'slow and not regression' --enable-cache | |
| - name: Upload coverage data | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: coverage-slow | |
| path: | | |
| .coverage | |
| coverage.xml | |
| include-hidden-files: true | |
| retention-days: 1 | |
| coverage: | |
| name: Combine Coverage | |
| # Only meaningful when the full suite ran (not on PRs). | |
| if: github.event_name != 'pull_request' | |
| needs: [test-fast, test-network, test-slow] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Set up Python | |
| uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 | |
| with: | |
| python-version: "3.11" | |
| cache: 'pip' | |
| - name: Install coverage | |
| run: pip install coverage[toml] | |
| - name: Download coverage data (fast) | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: coverage-fast | |
| path: coverage-fast/ | |
| - name: Download coverage data (network) | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: coverage-network | |
| path: coverage-network/ | |
| - name: Download coverage data (slow) | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: coverage-slow | |
| path: coverage-slow/ | |
| - name: Combine coverage | |
| run: | | |
| # Move .coverage files with unique names | |
| mv coverage-fast/.coverage .coverage.fast | |
| mv coverage-network/.coverage .coverage.network | |
| mv coverage-slow/.coverage .coverage.slow | |
| # Combine all coverage data | |
| coverage combine | |
| # Generate report and check threshold | |
| coverage report --fail-under=65 | |
| coverage xml -o combined-coverage.xml | |
| - name: Upload combined coverage to Codecov | |
| uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1 | |
| with: | |
| files: combined-coverage.xml | |
| fail_ci_if_error: false | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| # A red `main` has to arrive somewhere a person looks. | |
| # | |
| # Ported from regression-tests.yml, which got this treatment in #999 after the | |
| # post-merge regression lane went red on 2026-08-08 and three more pull | |
| # requests merged through it before anyone noticed. THIS workflow never got | |
| # one, and it is the lane that carries more: the 65% coverage floor | |
| # (`coverage report --fail-under=65`), plus test-network, test-slow and | |
| # test-strict-errors. All of them are post-merge only, none is a required check, and until now none of | |
| # them could tell anybody they had failed. | |
| # | |
| # Note what this deliberately does NOT do: make Combine Coverage required. | |
| # It cannot be. It `needs` test-network and test-slow, which are skipped on | |
| # pull requests, so a required Combine Coverage would never report and would | |
| # block every pull request forever — the deadlock described at the top of this | |
| # file. Reporting is the right mechanism for a check that cannot gate. | |
| # | |
| # `needs` names every post-merge job on purpose — including test-strict-errors | |
| # (bead edgartools-07lk.10), because a lane whose whole job is proving 6.0 | |
| # error behaviour still holds is worth nothing if it can go red in silence. | |
| # A failure in test-network | |
| # SKIPS Combine Coverage rather than failing it, so keying on the coverage | |
| # result alone would stay silent for the failure that stops the threshold | |
| # being evaluated at all. | |
| # | |
| # Its own label and title, separate from regression-tests.yml's `red-main`. | |
| # Sharing one issue would have the two lanes fight over it — whichever went | |
| # green last would close an issue the other still needed open. | |
| report: | |
| name: Report red main (build) | |
| needs: [cassette-gate, test-fast, test-strict-errors, test-network, test-slow, coverage] | |
| if: always() && github.event_name != 'pull_request' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| issues: write | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| REPO: ${{ github.repository }} | |
| RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| SHA: ${{ github.sha }} | |
| LABEL: red-main-build | |
| TITLE: "Build and Test Edgartools is failing on main" | |
| R_GATE: ${{ needs.cassette-gate.result }} | |
| R_FAST: ${{ needs.test-fast.result }} | |
| R_STRICT: ${{ needs.test-strict-errors.result }} | |
| R_NETWORK: ${{ needs.test-network.result }} | |
| R_SLOW: ${{ needs.test-slow.result }} | |
| R_COVERAGE: ${{ needs.coverage.result }} | |
| steps: | |
| - name: Ensure the tracking label exists | |
| run: | | |
| gh label create "$LABEL" --repo "$REPO" --color b60205 \ | |
| --description "Post-merge build/coverage lane is red" 2>/dev/null || true | |
| - name: Decide whether the lane is red | |
| id: verdict | |
| run: | | |
| red=0 | |
| for r in "$R_GATE" "$R_FAST" "$R_STRICT" "$R_NETWORK" "$R_SLOW" "$R_COVERAGE"; do | |
| [ "$r" = "failure" ] && red=1 | |
| done | |
| echo "red=$red" >> "$GITHUB_OUTPUT" | |
| - name: Open or update the tracking issue | |
| if: steps.verdict.outputs.red == '1' | |
| run: | | |
| number=$(gh issue list --repo "$REPO" --label "$LABEL" --state all \ | |
| --limit 1 --json number --jq '.[0].number // empty') | |
| # Name the job that failed. This lane has five, and "the build is red" | |
| # without saying which one sends the reader to the Actions tab to find | |
| # out — which is the step nobody takes. | |
| body=$(printf '%s\n\n- commit: %s\n- run: %s\n\n| job | result |\n| --- | --- |\n| Cassette safety gate | %s |\n| test-fast | %s |\n| test-strict-errors | %s |\n| test-network | %s |\n| test-slow | %s |\n| Combine Coverage (65%% floor) | %s |\n' \ | |
| "The post-merge build lane failed on \`main\`." "$SHA" "$RUN_URL" \ | |
| "$R_GATE" "$R_FAST" "$R_STRICT" "$R_NETWORK" "$R_SLOW" "$R_COVERAGE") | |
| if [ -z "$number" ]; then | |
| gh issue create --repo "$REPO" --label "$LABEL" \ | |
| --title "$TITLE" --body "$body" | |
| else | |
| gh issue reopen --repo "$REPO" "$number" 2>/dev/null || true | |
| gh issue comment --repo "$REPO" "$number" --body "$body" | |
| fi | |
| - name: Close the tracking issue when main is green again | |
| if: steps.verdict.outputs.red == '0' | |
| run: | | |
| number=$(gh issue list --repo "$REPO" --label "$LABEL" --state open \ | |
| --limit 1 --json number --jq '.[0].number // empty') | |
| if [ -n "$number" ]; then | |
| gh issue close --repo "$REPO" "$number" \ | |
| --comment "Green again at $SHA — $RUN_URL" | |
| fi |