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
135 changes: 135 additions & 0 deletions .github/workflows/project-reconcile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
name: Project Reconcile

on:
workflow_dispatch:
inputs:
apply:
description: Apply project membership changes after reviewing the report
required: true
default: false
type: boolean
schedule:
- cron: '17 3 * * *'

concurrency:
group: project-reconcile
cancel-in-progress: false

permissions:
contents: read

jobs:
reconcile:
name: Reconcile Project 28
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Reconcile organization issues
env:
GH_TOKEN: ${{ secrets.PROJECT_TOKEN }}
APPLY: ${{ inputs.apply || false }}
ORGANIZATION: z-shell
PROJECT_NUMBER: '28'
run: |
set -euo pipefail

if [[ -z "${GH_TOKEN}" ]]; then
echo 'PROJECT_TOKEN is not configured; refusing to run without a project-scoped credential.' >&2
exit 1
fi

project_id="$(gh api graphql -f query='
query($org: String!, $number: Int!) {
organization(login: $org) {
projectV2(number: $number) { id }
}
}' -f org="${ORGANIZATION}" -F number="${PROJECT_NUMBER}" --jq '.data.organization.projectV2.id')"

gh api --paginate \
-H 'Accept: application/vnd.github+json' \
"/search/issues?q=org%3A${ORGANIZATION}%20is%3Aissue%20is%3Aopen&per_page=100" \
--jq '.items[] | {url: .html_url, node_id: .node_id, title: .title, repository: .repository_url, author: .user.login, labels: [.labels[].name]}' \
> open-issues.ndjson

: > project-items.ndjson
cursor=''
while :; do
response="$(gh api graphql -f query='
query($project: ID!, $cursor: String) {
node(id: $project) {
... on ProjectV2 {
items(first: 100, after: $cursor) {
pageInfo { hasNextPage endCursor }
nodes {
id
content {
... on Issue { url node_id title repository { nameWithOwner } }
... on PullRequest { url node_id title repository { nameWithOwner } }
}
}
}
}
}
}' -f project="${project_id}" -f cursor="${cursor}")"
jq -c '.data.node.items.nodes[] | select(.content != null) | {item_id: .id, url: .content.url, node_id: .content.node_id, title: .content.title, repository: .content.repository.nameWithOwner}' <<<"${response}" >> project-items.ndjson
if [[ "$(jq -r '.data.node.items.pageInfo.hasNextPage' <<<"${response}")" != 'true' ]]; then
break
fi
cursor="$(jq -r '.data.node.items.pageInfo.endCursor' <<<"${response}")"
done

python3 - <<'PY'
import json
from pathlib import Path

def read_ndjson(name):
path = Path(name)
return [json.loads(line) for line in path.read_text().splitlines() if line]

issues = read_ndjson('open-issues.ndjson')
items = read_ndjson('project-items.ndjson')
item_nodes = {item['node_id'] for item in items}
missing = [issue for issue in issues if issue['node_id'] not in item_nodes]
report = {
'organization': 'z-shell',
'project_number': 28,
'apply_requested': '${{ inputs.apply || false }}' == 'true',
'open_issue_count': len(issues),
'project_content_count': len(items),
'missing_open_issues': missing,
}
Path('project-reconcile-report.json').write_text(json.dumps(report, indent=2) + '\n')
print(json.dumps({
'open_issue_count': len(issues),
'project_content_count': len(items),
'missing_open_issue_count': len(missing),
}, indent=2))
PY

if [[ "${APPLY}" != 'true' ]]; then
echo 'Dry run only; no project mutations requested.'
exit 0
fi

while IFS= read -r issue; do
node_id="$(jq -r '.node_id' <<<"${issue}")"
gh api graphql -f query='
mutation($project: ID!, $content: ID!) {
addProjectV2ItemById(input: {projectId: $project, contentId: $content}) {
item { id }
}
}' -f project="${project_id}" -f content="${node_id}" >/dev/null
done < <(jq -c '.missing_open_issues[]' project-reconcile-report.json)

echo 'Project membership reconciliation applied.'

- name: Upload reconciliation report
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: project-reconcile-report
path: |
open-issues.ndjson
project-items.ndjson
project-reconcile-report.json
if-no-files-found: warn
84 changes: 62 additions & 22 deletions runbooks/project-tracker.md
Original file line number Diff line number Diff line change
@@ -1,42 +1,82 @@
# Runbook Project tracker
# Runbook - Project tracker

Use this runbook for the organization-wide task tracking in Linear.
Use this runbook for organization-wide work tracking in GitHub Project 28,
`z-shell Delivery`.

## Tracker identity

- Workspace: `ss-o`
- URL: `https://linear.app/ss-o/`
- Organization: `z-shell`
- Project: `https://github.com/orgs/z-shell/projects/28`
- Project number: `28`

## What belongs on the tracker
GitHub Issues and pull requests remain the authoritative work records. Project
28 is the synchronized execution and portfolio view. Linear may mirror only
cross-repository, strategic, release-blocking, security-sensitive, or
organization-infrastructure work; it is not the source of truth.

Track issues that need cross-repository or maintainer-level attention:
## Inclusion policy

- cross-repository work
- release blockers
- security-sensitive work
- strategic or roadmap work
- organization infrastructure work
The organization-wide reconciler tracks all open organization issues so that
work cannot disappear between repositories. Project views separate workstreams:

Do not add ordinary single-repository bugs, support questions, or small cleanup tasks just because they are actionable.
- `Human delivery`: ordinary bugs, features, maintenance, and documentation
- `Automation`: bot dashboards and recurring automation records
- `Dependency maintenance`: routine dependency updates and dashboards
- `Security`: security work requiring maintainer attention
- `Administrative`: organization and repository governance

## Syncing with GitHub
Pull requests are tracked when they are linked to a tracked issue, ready for
review, or otherwise explicitly added by a maintainer. Closed and merged items
move to `Done` and are archived after the review window.

Linear natively integrates with GitHub issues and pull requests.
We rely on Linear's built-in GitHub integration rather than maintaining custom GitHub Actions workflows.
Do not create standalone project-only work for a deliverable that belongs in a
repository issue. Use a draft issue only for temporary capture, then convert or
discard it during triage.

1. Create or choose a test issue in the repository.
2. Because of the native integration, any issue matching the configured criteria in Linear will automatically be ingested.
3. You can manage and prioritize the issue directly from Linear.
## Project fields and relationships

At triage, set `Item Type`, `Impact`, `Effort`, and `Priority`. Set `Target date`
only for a real commitment. Use native parent issues and sub-issues for
coordinated outcomes, and native issue dependencies for blockers.

Keep cross-repository parent issues in `z-shell/.github` and implementation
issues in their owning repositories. Link implementation pull requests with a
closing keyword.

## Automation model

The built-in auto-add workflow is intentionally not the long-term reconciler:
its filter language cannot exclude bot authors and the GitHub Free plan allows
only one auto-add workflow. Keep it narrow while the central reconciler is
introduced.

The staged reconciler is read-only by default. It uses a project-scoped
credential supplied as `PROJECT_TOKEN` and accepts an explicit `apply=true`
manual dispatch only after review. It must be idempotent, emit a drift report,
and never overwrite human-set field values without a documented rule.

The target implementation is an organization-owned GitHub App with Project
read/write permission, installed on all repositories, receiving only the
required issue, pull-request, repository, and installation events. A scheduled
reconciliation remains as recovery for missed webhook deliveries.

## Verification

If an issue does not sync to Linear:
Run the dry-run workflow manually and inspect its artifact before enabling any
write mode. The report must identify:

- open organization issues missing from Project 28
- project items whose source issue or pull request is no longer visible
- unclassified workstream records
- bot and dependency-dashboard records
- project items with conflicting or missing relationship data

1. Verify the Linear GitHub integration settings are active for the specific repository.
2. Ensure you have not hit rate limits or permission boundaries.
3. Contact an organization admin if the repository needs to be manually added to the integration.
Any project membership, field, label, issue, repository, organization setting,
or workflow-setting mutation still requires explicit maintainer approval.

## See also

- `AGENTS.md`
- `runbooks/triage.md`
- `runbooks/labels.md`
- `decisions/`
Loading