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
1 change: 1 addition & 0 deletions .ai-context/COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions .ai-context/STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
8 changes: 8 additions & 0 deletions .ai-context/WORKFLOWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<category>/<issue>-<YYYYMMDD>-<slug>`.
Expand All @@ -13,6 +15,11 @@ Use the repo guidance in `AGENTS.md` and `CONTRIBUTING.md`:
`Closes #<issue>` 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:
Expand All @@ -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
```

Expand Down
2 changes: 2 additions & 0 deletions .github/base-project.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ project:
issue_defaults:
status: Backlog
priority: P2
area: Product
initiative: Adoption Polish
size: S
133 changes: 133 additions & 0 deletions .github/workflows/project-intake.yml
Original file line number Diff line number Diff line change
@@ -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"
13 changes: 13 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down
20 changes: 20 additions & 0 deletions tests/validate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Loading