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
7 changes: 6 additions & 1 deletion .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@

<!-- `Fixes #123` closes the issue when this PR is merged. Use `Refs #123` instead when the PR
resolves only part of it, e.g. some findings of a grouped issue. Quote any finding IDs
(CR-…, DOC-…, TECH-…) you address. Write "None" if there is no issue. -->
(CR-…, DOC-…, TECH-…) you address. Write "None" if there is no issue.
Put one keyword before each issue (`Fixes #12, fixes #13`): GitHub closes only the first of
`Fixes #12, #13`. The "Issue dependencies" check fails while an issue this PR links (title,
description, commits or the Development sidebar) is blocked, unless GitHub lists its blockers
as closed by this PR too, or a maintainer adds `dependency-override`. Mention an issue without
a keyword if this PR does not work on it. -->
Fixes #

## Changes
Expand Down
58 changes: 58 additions & 0 deletions .github/workflows/issue-dependencies.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: Issue dependencies

# Keeps the `blocked` label in step with each issue's native "Blocked by" links (the rules live in
# tools/issue_dependencies.py and CONTRIBUTING.md, "Dependencies between issues"):
# - an open issue with a blocker that is not closed as completed gets `blocked`;
# - every other issue loses it, with one comment when an open issue is unblocked;
# - a blocker closed as not planned or as a duplicate leaves the issue blocked and asks a maintainer to re-plan it;
# - every open pull request whose "Issue dependencies" check no longer matches its linked issues (red although
# they are unblocked, or green although one became blocked) gets that check re-run.
#
# GitHub sends no workflow event when a "Blocked by" link is added or removed, so this runs when an issue is closed
# or reopened (what unblocks or re-blocks its dependents), hourly as the backstop for link edits, and on demand.
# Every run is a full reconciliation of state, not a reaction to a transition, so the concurrency group may drop a
# pending run, and a run that fails half-way is finished by the next one. tests/test_issue_dependencies.py pins:
# - the token holds `contents: read`, `issues: write`, `pull-requests: read`, `checks: read` and `actions: write`,
# nothing else;
# - no `${{ }}` appears in a `run:` line; the script reads the environment itself;
# - the checkout takes the default branch, one file, no stored credentials, pinned to a full commit SHA.
#
# Maintainers: to stop it, open Actions > "Issue dependencies" > the "..." menu > "Disable workflow". The links
# stay; only the label stops following them.

on:
issues:
types: [closed, reopened]
schedule:
- cron: "17 * * * *"
workflow_dispatch:

permissions: {}

concurrency:
group: issue-dependencies
cancel-in-progress: false

jobs:
sync:
# This repository, and only its default branch: a manual run started from another branch would check out that
# branch's copy of the script with this job's write token.
if: ${{ github.repository == 'HodeTech/ForgeLM' && github.ref == 'refs/heads/development' }}
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read # the checkout reads one script from the default branch
issues: write # add or remove `blocked` / `needs-triage`, and post the notices
pull-requests: read # read the open pull requests and the issues they link
checks: read # read each one's latest "Issue dependencies" result
actions: write # re-run that check when the result no longer matches
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
sparse-checkout: tools/issue_dependencies.py
sparse-checkout-cone-mode: false
- name: Bring the blocked label in step with the Blocked-by links
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: python3 tools/issue_dependencies.py sync
58 changes: 58 additions & 0 deletions .github/workflows/pr-dependencies.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: PR dependencies

# The "Issue dependencies" check: fails while the pull request links an issue that is still blocked (Fixes / Closes /
# Resolves / Refs #N in its title, description or commits, or a Development-sidebar link), unless GitHub lists every
# open blocker as closed by the same pull request or a maintainer has labelled it `dependency-override`. Rules:
# tools/issue_dependencies.py and CONTRIBUTING.md, "Dependencies between issues". The "Issue dependencies" workflow
# re-runs this check whenever its result no longer matches the issues.
#
# Why `pull_request_target`. A `pull_request` run takes this file from the pull request itself, so a pull request
# could rewrite the check that judges it. `pull_request_target` takes the workflow and the script from the default
# branch instead. Its known risk is running the pull request's code with a privileged token; this job never does:
# the checkout has no `ref`, so it reads one file from the default branch; nothing from the pull request is checked
# out, built or executed; the token is read-only; no secret but GITHUB_TOKEN is used; and the script treats the
# description as data. tests/test_issue_dependencies.py pins these properties:
# - `pull_request_target` only, no `paths:` or `branches:` filter (a required check must always report);
# - the token holds `contents: read`, `issues: read` and `pull-requests: read`, nothing else;
# - the checkout has no `ref`, takes one file, keeps no credentials and is pinned to a full commit SHA, and no
# expression in this file names the pull request's head;
# - no `${{ }}` appears in a `run:` line; the script reads the event file itself.
# What it cannot stop: a pull request may add a different workflow with a job of the same name, and branch
# protection cannot tell two GitHub Actions checks of one name apart (true of every required Actions check), so a
# maintainer reads every change under `.github/` and `tools/` before merging it.
#
# Prerequisite: from 2 November 2026 GitHub blocks `pull_request_target` in public repositories unless an Actions
# policy allows it. This repository's policy (Settings > Actions > Policies) allows it for this file's path only;
# renaming or moving this file needs the policy updated in the same change, or the check stops reporting.
#
# One run per pull request at a time: a newer event cancels the run still going, so a burst of edits costs one run.

on:
pull_request_target:
types: [opened, edited, reopened, synchronize, ready_for_review, labeled, unlabeled]

permissions: {}

concurrency:
group: pr-dependencies-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
issue-dependencies:
name: Issue dependencies
runs-on: ubuntu-latest
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
timeout-minutes: 5
permissions:
contents: read # the checkout reads one script from the default branch
issues: read # read the linked issues, their "Blocked by" links and the label events
pull-requests: read # read the pull request's current description and labels
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
sparse-checkout: tools/issue_dependencies.py
sparse-checkout-cone-mode: false
- name: Fail if a linked issue is still blocked
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: python3 tools/issue_dependencies.py check-pr
8 changes: 7 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ ForgeLM/
│ # export, inference, results, utils, __main__,
│ # _pypdf_normalise, _script_sanity,
│ # _strip_pattern
├── tests/ # 131 test modules; count grows over time (run `ls tests/test_*.py | wc -l` for current)
├── tests/ # 132 test modules; count grows over time (run `ls tests/test_*.py | wc -l` for current)
├── tools/ # CI guards: check_anchor_resolution,
│ # check_bilingual_parity, check_cli_help_consistency,
│ # check_field_descriptions, check_no_analysis_refs,
Expand Down Expand Up @@ -387,6 +387,12 @@ Work on ForgeLM is planned and tracked on GitHub, not in local notes:
security vulnerability — never a public issue; it goes to a private advisory
as [SECURITY.md](SECURITY.md) describes.
- **Closing is not triage.** Every item is an issue; which issues appear on the front door is decided by labels and views, never by closing them.
- **Dependencies are links, not prose.** When an issue cannot start before
another is done, add a **Blocked by** link in its sidebar. The `blocked`
label, `/assign` and the **Issue dependencies** pull-request check
all follow those links (`tools/issue_dependencies.py`; CONTRIBUTING.md,
*Dependencies between issues*). Never set or remove `blocked` by hand, and do
not start work on a `blocked` issue.
- **Name what you resolved.** For a grouped issue, list in the pull request the
finding IDs your change resolves, so each can be ticked.

Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ All notable changes to ForgeLM are documented here.
when nobody holds the issue, refuses an issue that already has an assignee without changing it, allows at most 2
open assignments per contributor (maintainers are exempt) and leaves `security`, `needs-triage`, `deferred`,
epic and Phase 16 issues to a maintainer. Reassigning stays a maintainer's decision.
- Dependencies between issues are tracked and gated. A dependency is a native "Blocked by" link; the
`Issue dependencies` workflow (`tools/issue_dependencies.py`) labels every open issue with an unresolved blocker
`blocked` and removes the label, with one comment, once every blocker is closed as completed. A blocker closed as
not planned or as a duplicate keeps the issue blocked and flags it `needs-triage`. `/assign` refuses a blocked
issue, and the new `Issue dependencies` pull-request check fails a pull request that links one (`Fixes` /
`Closes` / `Resolves` / `Refs` in its title, description or commits, or a *Development* link), unless GitHub will
close the blockers with the same pull request or a maintainer sets `dependency-override`. The check runs from the
default branch (`pull_request_target`; no pull-request code is checked out), and the workflow re-runs it on every
open pull request whose result no longer matches. It is advisory until branch protection lists it (#1034).
- `GOVERNANCE.md` and `MAINTAINERS.md` at the repository root, recording how
decisions are made, how the maintainer roles work, and which areas the current
maintainer owns (#964).
Expand Down
8 changes: 7 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ ForgeLM/
│ # export, inference, results, utils, __main__,
│ # _pypdf_normalise, _script_sanity,
│ # _strip_pattern
├── tests/ # 131 test modules; count grows over time (run `ls tests/test_*.py | wc -l` for current)
├── tests/ # 132 test modules; count grows over time (run `ls tests/test_*.py | wc -l` for current)
├── tools/ # CI guards: check_anchor_resolution,
│ # check_bilingual_parity, check_cli_help_consistency,
│ # check_field_descriptions, check_no_analysis_refs,
Expand Down Expand Up @@ -387,6 +387,12 @@ Work on ForgeLM is planned and tracked on GitHub, not in local notes:
security vulnerability — never a public issue; it goes to a private advisory
as [SECURITY.md](SECURITY.md) describes.
- **Closing is not triage.** Every item is an issue; which issues appear on the front door is decided by labels and views, never by closing them.
- **Dependencies are links, not prose.** When an issue cannot start before
another is done, add a **Blocked by** link in its sidebar. The `blocked`
label, `/assign` and the **Issue dependencies** pull-request check
all follow those links (`tools/issue_dependencies.py`; CONTRIBUTING.md,
*Dependencies between issues*). Never set or remove `blocked` by hand, and do
not start work on a `blocked` issue.
- **Name what you resolved.** For a grouped issue, list in the pull request the
finding IDs your change resolves, so each can be ticked.

Expand Down
Loading
Loading