{"slug": "zero-my-project-factory", "title": "Zero: my project factory", "summary": "Developer Zander Martineau created 'zero', a project factory using a single GitHub Actions workflow that scaffolds new private repos from his starter templates, provisions Neon databases, sets secrets, and optionally kicks off Claude to build the project. The workflow, triggered via workflow_dispatch with inputs for name, idea, and type, supports 'app' (React + Neon) and 'astro' (Astro) templates, and includes steps to create the repo, wait for readiness, and create a Neon project for app-type projects.", "body_md": "In [my zero-to-one stack post](https://zander.wtf/blog/zero-to-one-stack) I ended with an admission: the one part of shipping side projects I hadn't cracked was the boring ceremony between \"I've just had an idea\" and \"something is actually being built\". Create a repo from a template, create a Neon project, copy secrets around, find the repo in Claude Code and explain what I want. None of it hard, all of it friction, and ideas die in that gap.\n\nI've now cracked it. The answer is a repo called **zero**: a project factory built from a single GitHub Actions workflow. I trigger it (from my phone, usually), give it a name, an idea, and a project type, and a few seconds later there's a new private repo scaffolded from one of my starter templates, with infrastructure provisioned, secrets set, and (optionally) Claude already working on the brief.\n\n**TL;DR**\n\n`workflow_dispatch`\n\nworkflow with three inputs: `name`\n\n, `idea`\n\n, `type`\n\n`type: app`\n\nscaffolds from `type: astro`\n\nscaffolds from `@claude build this`\n\nmention, so the first commit happens without meThe factory sits on top of the two starter templates I've written about before. [zed-stack-starter](https://github.com/mrmartineau/zed-stack-starter) is for interactive React applications: TanStack Router and Query, Hono on Cloudflare Workers, Postgres on Neon with Drizzle and better-auth. [zed-astro-starter](https://github.com/mrmartineau/zed-astro-starter) is for content-driven, mostly-static sites. Both use [ZUI](https://zui.zander.wtf), my CSS-first UI library, but otherwise they're quite different beasts — one needs a database, auth secrets, and an API; the other just needs to exist and deploy.\n\nThat difference is exactly what the workflow encodes. The `type`\n\ninput decides which template to generate from and how much infrastructure to bother with:\n\n`app`\n\n`DATABASE_URL`\n\nand a freshly generated `BETTER_AUTH_SECRET`\n\nas repo secrets, then add the Cloudflare deployment secrets.`astro`\n\nHere's the whole thing. It lives in the zero repo as `.github/workflows/create-project.yml`\n\n:\n\n```\nname: 🏭 New project\n\non:\n  workflow_dispatch:\n    inputs:\n      name:\n        description: Repo / project name\n        required: true\n        type: string\n      idea:\n        description: What should it do?\n        required: true\n        type: string\n      type:\n        description: Project type\n        required: true\n        type: choice\n        default: app\n        options:\n          - app # React + Neon (zed-stack-starter)\n          - astro # Astro (zed-astro-starter)\n      kickoff_claude:\n        description: Kick off Claude to build the project\n        required: false\n        type: boolean\n        default: false\n\nenv:\n  GH_TOKEN: ${{ secrets.FACTORY_GH_PAT }}\n  OWNER: mrmartineau\n  NAME: ${{ inputs.name }}\n  IDEA: ${{ inputs.idea }}\n\njobs:\n  create:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Pick template\n        id: template\n        run: |\n          if [ \"${{ inputs.type }}\" = \"astro\" ]; then\n            echo \"repo=zed-astro-starter\" >> \"$GITHUB_OUTPUT\"\n          else\n            echo \"repo=zed-stack-starter\" >> \"$GITHUB_OUTPUT\"\n          fi\n\n      - name: Create repo from template\n        run: |\n          gh api \"/repos/$OWNER/${{ steps.template.outputs.repo }}/generate\" \\\n            -f name=\"$NAME\" \\\n            -f owner=\"$OWNER\" \\\n            -F private=true \\\n            -f description=\"$IDEA\"\n\n      - name: Wait for repo to be ready\n        run: |\n          # template generation is async; poll until contents exist\n          for i in $(seq 1 12); do\n            if gh api \"/repos/$OWNER/$NAME/contents/README.md\" >/dev/null 2>&1; then\n              exit 0\n            fi\n            sleep 5\n          done\n          echo \"Repo never became ready\" >&2\n          exit 1\n\n      - name: Create Neon project\n        if: inputs.type == 'app'\n        id: neon\n        run: |\n          RESPONSE=$(curl -sf -X POST https://console.neon.tech/api/v2/projects \\\n            -H \"Authorization: Bearer ${{ secrets.NEON_API_KEY }}\" \\\n            -H \"Content-Type: application/json\" \\\n            -d \"{\\\"project\\\": {\\\"name\\\": \\\"$NAME\\\"}}\")\n          URI=$(echo \"$RESPONSE\" | jq -r '.connection_uris[0].connection_uri')\n          echo \"::add-mask::$URI\"\n          echo \"uri=$URI\" >> \"$GITHUB_OUTPUT\"\n\n      - name: Set app secrets\n        if: inputs.type == 'app'\n        run: |\n          gh secret set DATABASE_URL -R \"$OWNER/$NAME\" -b \"${{ steps.neon.outputs.uri }}\"\n          gh secret set BETTER_AUTH_SECRET -R \"$OWNER/$NAME\" -b \"$(openssl rand -base64 32)\"\n\n      - name: Set common secrets\n        run: |\n          gh secret set CLOUDFLARE_API_TOKEN -R \"$OWNER/$NAME\" -b \"${{ secrets.CLOUDFLARE_API_TOKEN }}\"\n          gh secret set CLOUDFLARE_ACCOUNT_ID -R \"$OWNER/$NAME\" -b \"${{ secrets.CLOUDFLARE_ACCOUNT_ID }}\"\n\n      - name: Kick off Claude\n        if: inputs.kickoff_claude\n        run: |\n          gh issue create -R \"$OWNER/$NAME\" \\\n            --title \"Build: $NAME\" \\\n            --body \"$IDEA\n\n          @claude build this. Update the readme, the Cloudflare project name, the package.json `name` field, and any other relevant files. Make sure the project is ready to run locally and deploy to Cloudflare Pages.\" \\\n            --label \"claude\"\n\n      - name: Summary\n        run: |\n          {\n            echo \"### 🎉 $NAME created\"\n            echo \"- Repo: https://github.com/$OWNER/$NAME\"\n            echo \"- Type: ${{ inputs.type }}\"\n          } >> \"$GITHUB_STEP_SUMMARY\"\n```\n\nA few details worth calling out:\n\n`/generate`\n\nendpoint`::add-mask::`\n\nstops the database URI from ever appearing in the workflow logs before it's passed between steps.`plant-tracker-3`\n\nwas supposed to be.The last optional input is my favourite bit. If `kickoff_claude`\n\nis ticked, the workflow opens an issue in the freshly created repo titled `Build: <name>`\n\n, with the idea as the body and an `@claude build this`\n\nmention. I have the [Claude GitHub app](https://docs.claude.com/en/docs/claude-code/github-actions) set up, so that mention is enough to get an agent cloning the repo and making the first real commits: renaming things, updating the README, and starting on the actual idea.\n\nWhich means the full loop is now: idea arrives while I'm out, I open the GitHub mobile app, run the **🏭 New project** workflow with a name and a couple of sentences, and put my phone back in my pocket. By the time I'm at a computer there's a repo with a database, deployments wired up, and a first pass at the build waiting for review. That's the \"one message to Claude\" experience I was after, give or take a form with three fields.\n\nThe factory repo itself needs four Actions secrets:\n\n| Secret | Purpose |\n|---|---|\n`FACTORY_GH_PAT` |\nGitHub PAT with permission to create repos, set secrets, and open issues in the new repo |\n`NEON_API_KEY` |\nNeon API key, used to create the Postgres project (`app` type) |\n`CLOUDFLARE_API_TOKEN` |\nPassed through to the new repo for deployments |\n`CLOUDFLARE_ACCOUNT_ID` |\nPassed through to the new repo for deployments |\n\nYou can trigger it from the Actions tab in the GitHub UI (or the mobile app), or from the CLI:\n\n```\ngh workflow run create-project.yml \\\n  -f name=my-new-app \\\n  -f idea=\"A tool that tracks my houseplants' watering schedules\" \\\n  -f type=app\n```\n\nSwap the templates, owner, and secrets for your own and the whole thing is portable. There's nothing here specific to my stack beyond which template repos it points at — if your starters are different, the factory doesn't care.\n\nThe thing I like most about this is how little there is to it. It's not a platform, there's no CLI to install, no service to pay for; it's ~100 lines of YAML gluing together APIs that already existed. But it removes the exact friction that was killing ideas between the thought and the first commit. Zero to one now starts from the sofa.", "url": "https://wpnews.pro/news/zero-my-project-factory", "canonical_source": "https://zander.wtf/blog/zero-project-factory/", "published_at": "2026-07-08 00:00:00+00:00", "updated_at": "2026-08-04 06:07:45.887154+00:00", "lang": "en", "topics": ["developer-tools", "ai-agents"], "entities": ["Zander Martineau", "GitHub Actions", "Neon", "Claude", "zed-stack-starter", "zed-astro-starter", "ZUI", "Cloudflare Workers"], "alternates": {"html": "https://wpnews.pro/news/zero-my-project-factory", "markdown": "https://wpnews.pro/news/zero-my-project-factory.md", "text": "https://wpnews.pro/news/zero-my-project-factory.txt", "jsonld": "https://wpnews.pro/news/zero-my-project-factory.jsonld"}}