diff --git a/.ai-context/COMMANDS.md b/.ai-context/COMMANDS.md index 46d2bac..30cdc33 100644 --- a/.ai-context/COMMANDS.md +++ b/.ai-context/COMMANDS.md @@ -10,6 +10,7 @@ basectl check bankbuddy basectl doctor bankbuddy basectl test bankbuddy basectl activate bankbuddy +basectl gh issue create --category enhancement --title "..." uv sync uv run pytest ./tests/validate.sh diff --git a/.ai-context/STATUS.md b/.ai-context/STATUS.md index 4b069d2..bc502e0 100644 --- a/.ai-context/STATUS.md +++ b/.ai-context/STATUS.md @@ -8,6 +8,7 @@ section is `Unreleased`. ## Current Product Capabilities - Base-managed setup, activation, and validation. +- Base-managed GitHub Project intake fallback for externally created issues. - Local SQLite initialization and ordered migrations. - Local data homes for `prod`, `dev`, and named environments. - Banking CLI commands for banks, accounts, statement refs, imports, diff --git a/.ai-context/WORKFLOWS.md b/.ai-context/WORKFLOWS.md index 8167a13..fed96e9 100644 --- a/.ai-context/WORKFLOWS.md +++ b/.ai-context/WORKFLOWS.md @@ -5,6 +5,8 @@ Use the repo guidance in `AGENTS.md` and `CONTRIBUTING.md`: 1. Create or choose a GitHub issue before implementation work. + Prefer `basectl gh issue create` so Base adds the issue to the repo Project + and applies `.github/base-project.yml` defaults. 2. Use one primary label: `bug`, `enhancement`, `documentation`, `ci`, or `security`. 3. Branch from `origin/main` with `/--`. @@ -13,6 +15,11 @@ Use the repo guidance in `AGENTS.md` and `CONTRIBUTING.md`: `Closes #` when merge should close the issue. 6. Run the relevant narrow tests and then the project validation command. +`.github/workflows/project-intake.yml` is the fallback for issues created +through the GitHub UI, plain `gh issue create`, or external connectors. It +adds or reconciles issues into the repo-named Project on open, reopen, close, +or manual dispatch when `BASE_PROJECT_TOKEN` has Project write access. + ## Validation The project validation command is: @@ -27,6 +34,7 @@ Useful narrower checks include: uv run pytest tests/test_tax_documents.py -q uv run pytest tests/test_imports.py -q uv run pytest tests/test_cli.py -q +basectl repo check . git diff --check ``` diff --git a/.github/base-project.yml b/.github/base-project.yml index 3fa0b81..258e4d4 100644 --- a/.github/base-project.yml +++ b/.github/base-project.yml @@ -4,4 +4,6 @@ project: issue_defaults: status: Backlog priority: P2 + area: Product + initiative: Adoption Polish size: S diff --git a/.github/workflows/project-intake.yml b/.github/workflows/project-intake.yml new file mode 100644 index 0000000..0efe16d --- /dev/null +++ b/.github/workflows/project-intake.yml @@ -0,0 +1,133 @@ +name: Project Intake + +on: + issues: + types: [opened, reopened, closed] + workflow_dispatch: + inputs: + issue_number: + description: Issue number to reconcile into the repo Project. + required: true + type: string + +permissions: + contents: read + issues: read + +jobs: + sync: + name: Sync issue Project fields + runs-on: ubuntu-latest + env: + BASE_PROJECT_OWNER: ${{ github.repository_owner }} + BASE_PROJECT_TITLE: ${{ github.event.repository.name }} + BASE_PROJECT_ISSUE_NUMBER: ${{ github.event.issue.number || inputs.issue_number }} + BASE_PROJECT_DEFAULT_OPEN_STATUS: Backlog + BASE_PROJECT_DEFAULT_CLOSED_STATUS: Done + BASE_PROJECT_DEFAULT_PRIORITY: P2 + BASE_PROJECT_DEFAULT_SIZE: S + BASE_PROJECT_DEFAULT_AREA: Product + BASE_PROJECT_DEFAULT_INITIATIVE: Adoption Polish + GH_TOKEN: ${{ secrets.BASE_PROJECT_TOKEN || github.token }} + steps: + - name: Reconcile Project item + shell: bash + run: | + set -euo pipefail + + issue_number="${BASE_PROJECT_ISSUE_NUMBER:-}" + if [[ -z "$issue_number" ]]; then + echo "::error::Issue number was not provided by the event or workflow_dispatch input." + exit 1 + fi + + issue_json="$(gh issue view "$issue_number" --repo "$GITHUB_REPOSITORY" --json state,url)" + issue_state="$(jq -r '.state' <<<"$issue_json")" + issue_url="$(jq -r '.url' <<<"$issue_json")" + + project_number="$( + gh project list --owner "$BASE_PROJECT_OWNER" --format json --limit 100 | + jq -r --arg title "$BASE_PROJECT_TITLE" \ + '.projects[] | select(.title == $title) | .number' | + head -n 1 + )" + if [[ -z "$project_number" ]]; then + echo "::error::GitHub Project '$BASE_PROJECT_TITLE' was not found for owner '$BASE_PROJECT_OWNER'." + exit 1 + fi + + project_id="$(gh project view "$project_number" --owner "$BASE_PROJECT_OWNER" --format json --jq '.id')" + item_id="$(gh project item-add "$project_number" --owner "$BASE_PROJECT_OWNER" --url "$issue_url" --format json --jq '.id')" + item_json="$( + gh project item-list "$project_number" --owner "$BASE_PROJECT_OWNER" --format json --limit 1000 | + jq --arg id "$item_id" '.items[] | select(.id == $id)' + )" + fields_json="$(gh project field-list "$project_number" --owner "$BASE_PROJECT_OWNER" --format json)" + + field_id_for() { + local field_name="$1" + + jq -r --arg name "$field_name" \ + '.fields[] | select(.name == $name) | .id' <<<"$fields_json" | + head -n 1 + } + + option_id_for() { + local field_name="$1" + local option_name="$2" + + jq -r --arg name "$field_name" --arg option "$option_name" \ + '.fields[] | select(.name == $name) | .options[]? | select(.name == $option) | .id' \ + <<<"$fields_json" | + head -n 1 + } + + set_single_select() { + local field_name="$1" + local option_name="$2" + local field_id + local option_id + + [[ -n "$option_name" ]] || return 0 + + field_id="$(field_id_for "$field_name")" + option_id="$(option_id_for "$field_name" "$option_name")" + if [[ -z "$field_id" || -z "$option_id" ]]; then + echo "::error::Project field '$field_name' option '$option_name' was not found." + exit 1 + fi + + gh project item-edit \ + --id "$item_id" \ + --project-id "$project_id" \ + --field-id "$field_id" \ + --single-select-option-id "$option_id" \ + >/dev/null + } + + set_single_select_if_missing() { + local field_name="$1" + local item_key="$2" + local option_name="$3" + local current_value + + current_value="$(jq -r --arg key "$item_key" '.[$key] // ""' <<<"$item_json")" + if [[ -n "$current_value" ]]; then + return 0 + fi + + set_single_select "$field_name" "$option_name" + } + + status_value="$BASE_PROJECT_DEFAULT_OPEN_STATUS" + if [[ "$issue_state" == "CLOSED" ]]; then + status_value="$BASE_PROJECT_DEFAULT_CLOSED_STATUS" + fi + + set_single_select Status "$status_value" + set_single_select_if_missing Priority priority "$BASE_PROJECT_DEFAULT_PRIORITY" + set_single_select_if_missing Size size "$BASE_PROJECT_DEFAULT_SIZE" + set_single_select_if_missing Area area "$BASE_PROJECT_DEFAULT_AREA" + set_single_select_if_missing Initiative initiative "$BASE_PROJECT_DEFAULT_INITIATIVE" + + printf 'Synced issue #%s into Project %s.\n' "$issue_number" "$BASE_PROJECT_TITLE" diff --git a/AGENTS.md b/AGENTS.md index 485159c..c392fef 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,6 +6,9 @@ precedence over this baseline. ## Workflow 1. Create or choose a GitHub issue before implementation work. + Prefer `basectl gh issue create` over plain `gh issue create` so Base can + add the issue to the repo-named GitHub Project and apply + `.github/base-project.yml` defaults immediately. 2. Use one standard issue label: `bug`, `enhancement`, `documentation`, `ci`, or `security`. 3. Branch from the issue with: @@ -26,6 +29,16 @@ precedence over this baseline. 6. Preserve existing user changes. Do not overwrite project-owned files unless the user explicitly asks for that edit. +If an issue is created through the GitHub UI, plain `gh issue create`, or an +external connector, the Project Intake workflow should add it to the repo +Project. If Project fields still look wrong, run `basectl repo configure` or +`basectl gh project issue set-fields` to reconcile the item before starting +implementation. + +The Project Intake workflow needs a `BASE_PROJECT_TOKEN` Actions secret with +GitHub Project write access when the default `GITHUB_TOKEN` cannot update the +user-owned Project. Keep that token in GitHub Actions secrets, not in the repo. + ## Validation Run the project validation command before publishing changes: diff --git a/tests/validate.sh b/tests/validate.sh index 7fcd760..fe2f5e5 100755 --- a/tests/validate.sh +++ b/tests/validate.sh @@ -6,9 +6,11 @@ required_files=( CHANGELOG.md CONTRIBUTING.md .github/pull_request_template.md + .github/base-project.yml LICENSE base_manifest.yaml Brewfile + .github/workflows/project-intake.yml .github/workflows/tests.yml pyproject.toml src/bankbuddy/__init__.py @@ -25,6 +27,24 @@ done printf 'Repository baseline is present.\n' +for default in \ + "status: Backlog" \ + "priority: P2" \ + "area: Product" \ + "initiative: Adoption Polish" \ + "size: S" +do + grep -Fq "$default" .github/base-project.yml || { + printf 'Missing Project issue default: %s\n' "$default" >&2 + exit 1 + } +done + +grep -Fq "BASE_PROJECT_TOKEN" .github/workflows/project-intake.yml || { + printf 'Project intake workflow must use BASE_PROJECT_TOKEN fallback.\n' >&2 + exit 1 +} + if [[ -f pyproject.toml ]]; then uv run pytest fi