# How to manage your GitHub project like a product team

> Building is the easy part now. Here's the system I use to plan, check and ship my own projects on GitHub, so a person or an AI agent can run them the same way every time.

Published: 2026-10-12T09:00:00.000Z
Author: Shameem Reza
Category: Development
Canonical: https://shameemreza.com/manage-github-project-like-product-team/

---

import Tldr from '../../components/Tldr.astro';

Building something isn't the hard part anymore. With AI, a weekend is enough to get a working plugin, a site or an app.

What's hard is everything after it. The second version. The bug report from someone you've never met. The change you make in a hurry six months later, when you've forgotten how half of it works.

That's where a project either starts acting like a product, or doesn't. The difference is rarely the code. It's the system around it: how work gets planned, checked, shipped and remembered.

AI makes the building fast. It doesn't do the product management or the system design for you: deciding what gets built next, what "done" means, and what keeps a bad change out. That's the part that decides whether a project grows or quietly stalls.

This is the system I use for my own projects. It's set up so that I, or an AI agent working with me, can run a project the same way every time. My WordPress plugin [HappyAccess](/happyaccess-wordpress-temporary-admin-access/) is the example all the way through, and every file and command is at the end, ready to copy.

<Tldr>
Treat every change, even a typo, as planned work: an issue, a branch, a pull request. Make one CI check, "CI passed", the only gate into a protected main branch, and apply it to yourself too. Plan versions with milestones, sort work with type and area labels, and release by tagging main. Then write the same rules down for your AI agent.
</Tldr>

## Decide how the project runs before it grows

Most projects get their process by accident. The first commits go straight to main, because there's nobody else around. A year later the project still has no rules, and every change is a small gamble.

I decide the rules once, early, and then I don't argue with them. Every change starts as an issue. Every issue gets its own branch, and every branch comes back through a pull request that only a green check lets into main.

It sounds heavy for one person, I know. It actually costs a minute per change, and it buys something you can't get later: a history where every change has a reason, a test run and a link back to why it happened.

That history is what a new contributor reads first. It's also what an AI agent needs most, because an agent can't remember last month's decisions unless they're written down somewhere it can find.

## Make the plan visible

A plan that lives in your head can't be shared, checked or handed off. On GitHub, the plan is three things: issues, labels and milestones.

Issues are the work. Each one says what the problem is from the user's side, how I'd approach it, and what "done" means, as a checklist. When every box is ticked, the issue is finished, and nobody has to guess.

Labels sort the work two ways. A type label says what kind of change it is: `bug`, `enhancement` or `documentation`. An area label says which part of the product it touches, like `area:two-step` or `area:core` in HappyAccess. With both, one filter shows exactly what's planned for one part of the next version.

![The HappyAccess issues on GitHub, each with a type label, an area label such as area:two-step or area:core, and a milestone of 1.2.0 or Later](https://shameemreza.com/_astro/github-issues-labels-milestones.mgFV1wjG.png)

Milestones are versions. I keep two open at a time: the next release, and "Later" for ideas that aren't planned yet. "Later" matters more than it looks. It's where good ideas wait without cluttering the next release.

![The 1.2.0 milestone on GitHub with eight open issues, each labeled with its type and area](https://shameemreza.com/_astro/github-milestone-1-2-0.CaVrZ3qN.png)

After HappyAccess 1.1.0 shipped, I compared it with 15 plugins that solve similar problems and wrote up every gap worth closing. That turned into 16 issues: 8 for 1.2.0, one big feature with a release of its own, and 7 for later. The same day, a developer asked under [my LinkedIn post](https://lnkd.in/p/gcPAmF9G) whether HappyAccess could delete activity logs. That became an issue too, in the 1.2.0 milestone, and I replied with the link.

That's the habit I'd copy first. Feedback and research turn into issues the same day. Otherwise they end up in a list somewhere that nobody reads again.

## Let people tell you the right things

A blank "new issue" box gets you "it doesn't work". Then the next three messages are you asking which version, which steps and what they expected.

Issue forms ask those questions up front. GitHub shows them as a short form with required fields, and it adds the right label for you, so a new bug report arrives already labeled. I also turn off blank issues and point security reports to a private channel, so nobody posts a vulnerability in public by mistake.

Pull requests get a template too. Mine is four short headings: what changed, why, how I tested it, and a `Closes #` line. That last line links the pull request to its issue, and merging it closes the issue on its own. The plan and the code stay connected without any extra work.

## One quality gate that never moves

This decision looks small, and it saves a lot of trouble later.

HappyAccess runs its tests on PHP 7.4 and 8.4, against three versions of WooCommerce, on single sites and multisite. That's seven CI jobs, and each one has its version in its name. The obvious move is to make all seven required.

It's tempting, and it backfires. The day a new WooCommerce version comes out and you update the test matrix, the job names change. GitHub keeps waiting for checks named after the old versions, and every pull request is stuck until you go and fix the settings.

So I add one job at the very end, named "CI passed". It waits for every other job and only goes green if they all passed. That one name is the only required check. I can change versions, add jobs or remove them, and the gate never moves.

![A GitHub Actions run where the PHPUnit matrix, the PHPCS job and the JavaScript job all lead into one final job named CI passed](https://shameemreza.com/_astro/github-actions-ci-passed-job.CvTfRGRK.png)

The jobs themselves depend on the project. For a WordPress plugin, I run PHPUnit, [PHPCS with the WordPress coding standards](/phpcs-with-wordpress-coding-standards/) and [PHPStan](/phpstan-for-wordpress-and-woocommerce/). For a React, Next.js or Astro site, it's lint, tests and the build. For a Swift app, it's the build and the tests on a macOS runner. The final gate looks the same in all of them.

## Rules that apply to the owner too

A protected main branch means nothing reaches main except through a pull request with a green "CI passed". On GitHub, that's branch protection.

The setting I care about most is the one that includes admins. Without it, I'm the owner, so I can skip the rules whenever I like. And the days I'd want to skip them are exactly the days I shouldn't: late, tired, in a hurry, sure that a one-word fix can't break anything.

I leave required reviews off. On a solo project there's nobody to approve, so CI does the reviewing. I also make squash-merge the only way to merge, so each pull request becomes one commit on main, and the history reads like a list of changes, one per issue.

## A rhythm for every change

Once the rules are in place, every change follows the same six steps. It feels like ceremony for the first week. After that, it's just how work happens.

![A hand-drawn guide to the six steps of every change: open an issue, branch from main, commit and push, open a pull request, wait for CI passed, then merge and release](https://shameemreza.com/_astro/github-every-change-six-steps.C_hABshc.png)

The branch name says what kind of change it is and which issue it belongs to, like `fix/12-readme-typo` or `feat/2-delete-logs`. Anyone looking at the branch list, including future me, knows what each one is for.

The first pull request on the HappyAccess repo was the "CI passed" change itself. It went through the same steps as everything after it.

![HappyAccess pull request number 1, Add one CI passed check for branch protection, merged into main with 8 checks passed](https://shameemreza.com/_astro/github-pull-request-ci-passed.Bbxt8O3e.png)

## Release when the milestone is done

A release isn't a date. It's a milestone with every issue closed.

When that happens, one last pull request bumps the version and the changelog. Then I tag the release from main and let GitHub write the release notes from the merged pull requests. That only works well because every pull request has a clear title and closes a real issue, which is one more reason the rhythm above matters.

What happens after the tag depends on the product. My website deploys on every merge to main. A WordPress plugin still goes out through the [SVN release on WordPress.org](/upload-wordpress-plugin-svn-using-mac/), and that step can run from a GitHub Action on the tag too.

## Write the system down for your AI agent

I build with an AI agent, so the last step was writing all of this down where the agent reads it. I keep it as a skill file: the rules, the templates, the CI recipes for each stack, and one setup script that applies everything to a new repo in a minute.

Then I tested it the way I'd test anything else. I asked an agent for a one-word typo fix and told it I was in a hurry. Without the written rules, it committed straight to main, a commit the protected branch would have rejected. With the rules, it created a branch and a pull request, and handed me the commands.

The rules also check who owns the repo first. My own projects follow this system. Repos that belong to a team or a client follow that team's process, so the agent leaves them alone.

An agent writes code faster than any of us. A written system is how you stay the product manager of your own project, the one deciding what ships.

## The whole setup, ready to copy

Replace `OWNER/REPO` with your own. Run the commands from your project folder, in this order.

### Files

```text
.github/
  ISSUE_TEMPLATE/
    bug_report.yml
    feature_request.yml
    config.yml
  PULL_REQUEST_TEMPLATE.md
  workflows/
    ci.yml
README.md
CONTRIBUTING.md
SECURITY.md
CHANGELOG.md
LICENSE
```

### A bug report form

`.github/ISSUE_TEMPLATE/bug_report.yml`, shortened. The feature request form follows the same shape, with "What's the problem?" and "What would you like it to do?".

```yaml
name: Bug report
description: Something doesn't work the way it should.
labels: [ "bug" ]
body:
  - type: textarea
    id: what-happened
    attributes:
      label: What happened?
      description: What you saw, including any error message, word for word.
    validations:
      required: true

  - type: textarea
    id: steps
    attributes:
      label: Steps to reproduce
      placeholder: |
        1. Go to ...
        2. Click ...
        3. See the error.
    validations:
      required: true

  - type: input
    id: version
    attributes:
      label: Version
    validations:
      required: true

  - type: checkboxes
    id: checks
    attributes:
      label: Before you send it
      options:
        - label: I searched the open and closed issues, and this isn't one of them.
          required: true
```

`.github/ISSUE_TEMPLATE/config.yml`:

```yaml
blank_issues_enabled: false
contact_links:
  - name: Report a security issue
    url: https://github.com/OWNER/REPO/security/policy
    about: Please report security issues privately, not in a public issue.
```

### The pull request template

`.github/PULL_REQUEST_TEMPLATE.md`:

```markdown
Closes #

## What changed

## Why

## How I tested it

- [ ] CI passed
- [ ] Tested by hand:
```

### CI with the one gate

`.github/workflows/ci.yml` for a Node project. Swap the build job for your stack, and add every job to `needs` and to the final check.

```yaml
name: CI

on:
  push:
    branches: [ main ]
  pull_request:

jobs:
  build:
    name: Lint, test and build
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version-file: .nvmrc
          cache: npm
      - run: npm ci
      - run: npm run lint --if-present
      - run: npm test --if-present
      - run: npm run build

  ci-passed:
    name: CI passed
    if: always()
    needs: [ build ]
    runs-on: ubuntu-24.04
    steps:
      - name: Check that every job passed
        run: test "${{ needs.build.result }}" = success
```

`if: always()` makes the final job run even after a failure, so it reports the failure instead of being skipped.

If your first push of this file fails with "refusing to allow an OAuth App to create or update workflow", your GitHub CLI login needs one more permission:

```bash
gh auth refresh -h github.com -s workflow
```

### Protect main

Push the CI file and let it run once. Then:

```bash
gh api -X PUT repos/OWNER/REPO/branches/main/protection --input - <<'EOF'
{
  "required_status_checks": { "strict": true, "contexts": [ "CI passed" ] },
  "enforce_admins": true,
  "required_pull_request_reviews": null,
  "restrictions": null,
  "allow_force_pushes": false,
  "allow_deletions": false
}
EOF

gh repo edit OWNER/REPO --enable-squash-merge --enable-merge-commit=false --enable-rebase-merge=false --delete-branch-on-merge
```

### Labels and milestones

```bash
gh label create "area:core" --color 6a737d --description "Settings, install, shared code" -R OWNER/REPO
gh label create "area:admin" --color 1d76db --description "The admin screens" -R OWNER/REPO

gh api repos/OWNER/REPO/milestones -f title="1.0.0"
gh api repos/OWNER/REPO/milestones -f title="Later"
```

### Security settings

```bash
gh api -X PUT repos/OWNER/REPO/private-vulnerability-reporting
gh api -X PUT repos/OWNER/REPO/vulnerability-alerts
gh api -X PATCH repos/OWNER/REPO --input - <<'EOF'
{"security_and_analysis":{"secret_scanning":{"status":"enabled"},"secret_scanning_push_protection":{"status":"enabled"}}}
EOF
```

These turn on private vulnerability reports, Dependabot alerts for your dependencies, and a block on any push that contains a key or a token.

### Every change

```bash
git switch main && git pull
git switch -c docs/12-readme-typo

# make the change, then:
git add README.md
git commit -m "Fix the receive typo in the README"
git push -u origin docs/12-readme-typo
gh pr create --title "Fix the receive typo in the README" --body "Closes #12"

# when CI passed is green:
gh pr merge docs/12-readme-typo --squash --delete-branch
```

### Every release

```bash
git switch main && git pull
git tag v1.0.0
git push origin v1.0.0
gh release create v1.0.0 --generate-notes
```

You can see all of this running on the [HappyAccess repo on GitHub](https://github.com/shameemreza/happyaccess): the issues, the milestones, and the checks on every pull request. Open an issue there if you spot something I'd do better, and you'll see the same process from the other side.

If you set this up on your own project, start with the "CI passed" check and the protected main. Everything else grows from there.

I'd like to hear how it goes. Tell me in the comments what stack you used it on and what you changed, or what still feels messy in how you run your project.
