diff --git a/.github/workflows/project-reconcile.yml b/.github/workflows/project-reconcile.yml new file mode 100644 index 000000000..ba5a65bc5 --- /dev/null +++ b/.github/workflows/project-reconcile.yml @@ -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 \ No newline at end of file diff --git a/runbooks/project-tracker.md b/runbooks/project-tracker.md index d55546adb..17c26c093 100644 --- a/runbooks/project-tracker.md +++ b/runbooks/project-tracker.md @@ -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/`