{"id":46035457,"url":"https://github.com/siner308/staqd","last_synced_at":"2026-03-09T11:07:31.102Z","repository":{"id":337732466,"uuid":"1154936908","full_name":"siner308/staqd","owner":"siner308","description":"Stacked PR merge queue powered by GitHub Actions and PR comments. No CLI, no SaaS.","archived":false,"fork":false,"pushed_at":"2026-03-03T08:35:18.000Z","size":1303,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-03T11:21:36.577Z","etag":null,"topics":["automation","code-review","developer-tools","git-rebase","github-actions","merge-queue","pull-requests","stacked-prs"],"latest_commit_sha":null,"homepage":"https://siner308.github.io/staqd","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/siner308.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-11T00:00:04.000Z","updated_at":"2026-03-03T11:03:42.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/siner308/staqd","commit_stats":null,"previous_names":["siner308/graphite-without-graphite","siner308/staq"],"tags_count":8,"template":false,"template_full_name":null,"purl":"pkg:github/siner308/staqd","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/siner308%2Fstaqd","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/siner308%2Fstaqd/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/siner308%2Fstaqd/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/siner308%2Fstaqd/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/siner308","download_url":"https://codeload.github.com/siner308/staqd/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/siner308%2Fstaqd/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30291871,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-09T02:57:19.223Z","status":"ssl_error","status_checked_at":"2026-03-09T02:56:26.373Z","response_time":61,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["automation","code-review","developer-tools","git-rebase","github-actions","merge-queue","pull-requests","stacked-prs"],"created_at":"2026-03-01T05:21:57.042Z","updated_at":"2026-03-09T11:07:31.091Z","avatar_url":"https://github.com/siner308.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"docs/favicon.svg\" alt=\"Staqd\" width=\"80\" height=\"80\"\u003e\n\u003c/p\u003e\n\n\u003ch1 align=\"center\"\u003eStaqd\u003c/h1\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://opensource.org/licenses/MIT\"\u003e\u003cimg src=\"https://img.shields.io/badge/License-MIT-yellow.svg\" alt=\"License: MIT\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/siner308/staqd/releases\"\u003e\u003cimg src=\"https://img.shields.io/github/v/release/siner308/staqd?include_prereleases\u0026sort=semver\" alt=\"GitHub release\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n**/stakt/** — like \"stacked\"\n\nStacked PR merge queue powered by GitHub Actions and PR comments.\n\n## Usage\n\n### Basic\n\nAdd a workflow file for each of the two events:\n\n`.github/workflows/staqd-auto-detect.yml` — runs on pull request events:\n\n```yaml\nname: Staqd Auto Detect\n\non:\n  pull_request:\n    types: [opened, edited, closed]\n\npermissions:\n  pull-requests: write\n  issues: write\n\njobs:\n  staqd:\n    uses: siner308/staqd/.github/workflows/staqd-auto-detect.yml@v1\n```\n\n`.github/workflows/staqd-command.yml` — runs on PR comment commands:\n\n```yaml\nname: Staqd Command\n\non:\n  issue_comment:\n    types: [created]\n\npermissions:\n  contents: write\n  pull-requests: write\n  issues: write\n  checks: read\n\njobs:\n  staqd:\n    uses: siner308/staqd/.github/workflows/staqd-command.yml@v1\n```\n\nThat's it. Each workflow triggers only on its relevant event, keeping PR checks clean and focused.\n\n### With GitHub App (recommended for CI auto-trigger)\n\n`.github/workflows/staqd-auto-detect.yml`:\n\n```yaml\nname: Staqd Auto Detect\n\non:\n  pull_request:\n    types: [opened, edited, closed]\n\npermissions:\n  pull-requests: write\n  issues: write\n\njobs:\n  staqd:\n    uses: siner308/staqd/.github/workflows/staqd-auto-detect.yml@v1\n    with:\n      app-id: ${{ vars.STAQD_APP_ID }}\n    secrets:\n      app-private-key: ${{ secrets.STAQD_APP_PRIVATE_KEY }}\n```\n\n`.github/workflows/staqd-command.yml`:\n\n```yaml\nname: Staqd Command\n\non:\n  issue_comment:\n    types: [created]\n\npermissions:\n  contents: write\n  pull-requests: write\n  issues: write\n  checks: read\n\njobs:\n  staqd:\n    uses: siner308/staqd/.github/workflows/staqd-command.yml@v1\n    with:\n      app-id: ${{ vars.STAQD_APP_ID }}\n    secrets:\n      app-private-key: ${{ secrets.STAQD_APP_PRIVATE_KEY }}\n```\n\n\u003e **Deprecation:** The single-file `staqd.yml` is deprecated. Use the two-file setup above.\n\n### Reusable Workflow Inputs\n\n**`staqd-auto-detect.yml`**\n\n| Input | Description | Required | Default |\n|-------|-------------|----------|---------|\n| `app-id` | GitHub App ID. Used with `app-private-key` to generate an installation token. | No | |\n| `runs-on` | Runner label for all jobs | No | `ubuntu-latest` |\n\n**`staqd-command.yml`**\n\n| Input | Description | Required | Default |\n|-------|-------------|----------|---------|\n| `app-id` | GitHub App ID. Used with `app-private-key` to generate an installation token. | No | |\n| `runs-on` | Runner label for all jobs | No | `ubuntu-latest` |\n| `timeout-minutes` | Timeout for command job | No | `30` |\n\n### Reusable Workflow Secrets\n\n| Secret | Description | Required |\n|--------|-------------|----------|\n| `app-private-key` | GitHub App private key. Used with `app-id` input. | No |\n\n\u003e **Why a GitHub App?** `GITHUB_TOKEN` pushes don't trigger other workflows (GitHub security policy). If your CI needs to run after a restack force-push, use a GitHub App.\n\n\u003cdetails\u003e\n\u003csummary\u003eHow to create a GitHub App\u003c/summary\u003e\n\n1. Go to **GitHub Settings → Developer settings → GitHub Apps → New GitHub App**\n2. Fill in:\n   - **App name**: e.g., `staqd-bot`\n   - **Homepage URL**: your repo URL\n   - **Webhook**: uncheck \"Active\" (not needed)\n3. Set **Repository permissions**:\n   - **Contents**: Read \u0026 write\n   - **Pull requests**: Read \u0026 write\n   - **Issues**: Read \u0026 write\n   - **Checks**: Read\n4. Click **Create GitHub App**\n5. Note the **App ID** from the app's settings page\n6. Click **Generate a private key** and download the `.pem` file\n7. Click **Install App** and install it on your repository\n8. In your repo, go to **Settings → Secrets and variables → Actions**:\n   - Add variable: `STAQD_APP_ID` = your App ID\n   - Add secret: `STAQD_APP_PRIVATE_KEY` = contents of the `.pem` file\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ch3\u003eAdvanced: Direct Composite Action\u003c/h3\u003e\u003c/summary\u003e\n\nFor full control over per-job runners, permissions, and custom steps, use the composite action directly:\n\n```yaml\nname: Staqd\n\non:\n  pull_request:\n    types: [opened, edited, closed]\n  issue_comment:\n    types: [created]\n\njobs:\n  # Guide comment — no concurrency needed\n  guide:\n    if: github.event_name == 'pull_request'\n    runs-on: ubuntu-latest\n    permissions:\n      pull-requests: write\n      issues: write\n    steps:\n      - uses: siner308/staqd@v1\n\n  # Resolve base branch for concurrency group\n  resolve-base:\n    if: \u003e-\n      github.event_name == 'issue_comment'\n      \u0026\u0026 github.event.issue.pull_request\n      \u0026\u0026 (startsWith(github.event.comment.body, 'stack ') || startsWith(github.event.comment.body, 'st '))\n    runs-on: ubuntu-latest\n    permissions:\n      pull-requests: read\n    outputs:\n      base-ref: ${{ steps.get.outputs.base-ref }}\n    steps:\n      - id: get\n        uses: actions/github-script@v7\n        with:\n          script: |\n            const { data: pr } = await github.rest.pulls.get({\n              ...context.repo,\n              pull_number: context.payload.issue.number,\n            });\n            core.setOutput('base-ref', pr.base.ref);\n\n  # Execute command — serialized per base branch\n  command:\n    needs: resolve-base\n    concurrency:\n      group: staqd-${{ needs.resolve-base.outputs.base-ref }}\n      cancel-in-progress: false\n    runs-on: ubuntu-latest\n    timeout-minutes: 30\n    permissions:\n      contents: write\n      pull-requests: write\n      issues: write\n      checks: read\n    steps:\n      - uses: siner308/staqd@v1\n```\n\n#### Composite Action Inputs\n\n| Input | Description | Required | Default |\n|-------|-------------|----------|---------|\n| `token` | GitHub token for API calls and git operations. Use a PAT or GitHub App token to enable CI auto-trigger after force push. | No | `github.token` |\n| `app-id` | GitHub App ID. Used with `app-private-key` to generate an installation token. | No | |\n| `app-private-key` | GitHub App private key. Used with `app-id` to generate an installation token. | No | |\n\n\u003c/details\u003e\n\n## Commands\n\nComment on a PR to trigger:\n\n| Command | Action |\n|---------|--------|\n| `stack merge` (`st merge`) | Auto-discover stack, merge this PR, restack child branches, and delete the merged branch |\n| `stack merge-all` (`st merge-all`) | Auto-discover stack, merge the entire stack in order, deleting each branch after merge (all PRs must be approved) |\n| `stack merge-all --force` (`st merge-all --force`) | Same as `merge-all` but skips the approval check |\n| `stack restack` (`st restack`) | Restack entire stack recursively |\n| `stack discover` (`st discover`) | Auto-discover stack tree from base branches and update metadata |\n| `stack help` (`st help`) | Show usage |\n\n\u003e **Tip:** All commands support the short alias `st` — e.g., `st merge` instead of `stack merge`.\n\n## Setting Up a Stack\n\n### 1. Create stacked branches\n\n```bash\ngit checkout main\ngit checkout -b feat-auth\n# ... make changes, push ...\n\ngit checkout feat-auth\ngit checkout -b feat-auth-ui\n# ... make changes, push ...\n\ngit checkout feat-auth-ui\ngit checkout -b feat-auth-tests\n# ... make changes, push ...\n```\n\n### 2. Create PRs with correct base branches\n\n```bash\ngh pr create --base main       --head feat-auth       --title \"feat: add auth module\"   # → PR #1\ngh pr create --base feat-auth  --head feat-auth-ui    --title \"feat: add auth UI\"       # → PR #2\ngh pr create --base feat-auth-ui --head feat-auth-tests --title \"test: add auth tests\"  # → PR #3\n```\n\n### 3. Add stack metadata\n\n**Option A: Auto-discover (recommended)**\n\nComment on the root PR:\n\n```\nst discover\n```\n\nStaqd scans all open PRs by base branch relationships, builds the tree, and updates every PR's metadata automatically. It also detects if any children need restacking:\n\n```\n### Stack Discovered\n\nFound 3 PR(s) in the stack:\n\n- #1 (`feat-auth`)\n  - #2 (`feat-auth-ui`) ⚠️\n    - #3 (`feat-auth-tests`)\n\nAll PR metadata has been updated.\n\n\u003e ⚠️ Some PRs are out of date with their parent branch.\n\u003e Run `st restack` on the parent PR to rebase.\n```\n\n**Option B: Manual metadata**\n\nEach parent PR's body needs an HTML comment listing its direct children:\n\n```bash\n# PR #1 body — child is PR #2\ngh api repos/{owner}/{repo}/pulls/1 --method PATCH --input - \u003c\u003c'EOF'\n{\"body\": \"Add auth module\\n\\n\u003c!-- stack-rebase:{\\\"children\\\":[{\\\"branch\\\":\\\"feat-auth-ui\\\",\\\"pr\\\":2}]} --\u003e\"}\nEOF\n\n# PR #2 body — child is PR #3\ngh api repos/{owner}/{repo}/pulls/2 --method PATCH --input - \u003c\u003c'EOF'\n{\"body\": \"Add auth UI\\n\\n\u003c!-- stack-rebase:{\\\"children\\\":[{\\\"branch\\\":\\\"feat-auth-tests\\\",\\\"pr\\\":3}]} --\u003e\"}\nEOF\n\n# PR #3 — no children, no metadata needed\n```\n\n\u003e **Note:** Use `gh api` instead of `gh pr edit --body` because some shells strip HTML comments.\n\n### 4. Use commands\n\n```\n# During development — sync children after pushing to parent\nComment on PR #1: \"stack restack\"\n\n# After review — merge the entire stack at once\nComment on PR #1: \"stack merge-all\"\n```\n\n## Metadata Format\n\n```html\n\u003c!-- stack-rebase:{\"children\":[{\"branch\":\"\u003cbranch\u003e\",\"pr\":\u003cnumber\u003e}]} --\u003e\n```\n\nOnly list **direct children**. `merge-all` recursively follows each child's metadata.\n\n**Linear stack** (A → B → C):\n```\nPR #1 body: \u003c!-- stack-rebase:{\"children\":[{\"branch\":\"B\",\"pr\":2}]} --\u003e\nPR #2 body: \u003c!-- stack-rebase:{\"children\":[{\"branch\":\"C\",\"pr\":3}]} --\u003e\nPR #3 body: (none)\n```\n\n**Tree stack** (X and Y branch from base):\n```\nPR #10 body: \u003c!-- stack-rebase:{\"children\":[{\"branch\":\"X\",\"pr\":11},{\"branch\":\"Y\",\"pr\":12}]} --\u003e\nPR #11 body: (none)\nPR #12 body: (none)\n```\n\nTree children are siblings — each rebases independently onto the same parent.\n\n## How It Works\n\nEverything reduces to one git command:\n\n```\ngit rebase --onto \u003cnew_base\u003e \u003cold_parent_tip_sha\u003e \u003cchild_branch\u003e\n```\n\n```\nBefore (feat-1 squash-merged into main):\n\nmain:     A───B───CD'          (C+D squashed into new commit)\nfeat-2:       C───D───E───F    (still contains original C, D)\n\nAfter:\ngit rebase --onto main \u003cSHA_of_D\u003e feat-2\n\nmain:     A───B───CD'\n                    \\\nfeat-2:              E'───F'   (C, D removed; only E, F rebased)\n```\n\nThe skip SHA (`old_parent_tip_sha`) comes from GitHub's PR API, which preserves `head.sha` even after merge. No database needed.\n\n### Workflow\n\n```mermaid\nsequenceDiagram\n    participant Dev as Developer\n    participant PR as PR Comment\n    participant GA as GitHub Actions\n    participant Git as Git / GitHub API\n\n    Dev-\u003e\u003ePR: stack merge\n    PR-\u003e\u003eGA: Trigger workflow\n    GA-\u003e\u003eGit: Merge PR (squash)\n    GA-\u003e\u003eGit: git fetch origin\n    GA-\u003e\u003eGit: git rebase --onto main skip_sha child\n    GA-\u003e\u003eGit: git push --force-with-lease\n    GA-\u003e\u003ePR: Post result comment\n\n    Note over Dev,Git: merge-all repeats this for each child (DFS)\n```\n\n## Handling Conflicts\n\nWhen a conflict occurs, the bot posts manual fix commands:\n\n```\nRestack: Action Needed\n\n| Branch | PR | Status |\n|--------|-----|--------|\n| feat-auth-ui | #2 | Restacked |\n| feat-auth-tests | #3 | Conflict |\n\nManual restack commands:\n  git fetch origin\n  git rebase --onto origin/feat-auth-ui \u003cskip_sha\u003e feat-auth-tests\n  # resolve conflicts\n  git push origin feat-auth-tests --force-with-lease\n```\n\n## Design Decisions\n\n\u003cdetails\u003e\n\u003csummary\u003eWhy HTML comments in PR body?\u003c/summary\u003e\n\n- No metadata files polluting the codebase\n- Readable/writable via GitHub API — no separate storage\n- Invisible when rendered — no noise for reviewers\n- PR body is deterministic (one per PR); comments checked as fallback\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eWhy only direct children (not all descendants)?\u003c/summary\u003e\n\n- Enables tree-shaped stacks where one parent has multiple children\n- All children are siblings — each rebases independently (no chaining)\n- `merge-all` recursively traverses each child's metadata (DFS)\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eWhy different --onto targets for restack vs merge?\u003c/summary\u003e\n\n| Scenario | `--onto` target | Reason |\n|----------|----------------|--------|\n| `stack restack` (pre-merge) | `origin/\u003cparent_branch\u003e` | Must include parent's new commits |\n| `stack merge` (post-merge) | `origin/main` | Parent already merged into main |\n| `stack merge-all` | `origin/main` | Each PR merges into main sequentially |\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eWhy --force-with-lease?\u003c/summary\u003e\n\nRejects the push if the remote ref changed since last fetch. Prevents overwriting commits pushed by others.\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eWhy retry merge API instead of polling checks API?\u003c/summary\u003e\n\n- Check suite names vary by repo — would require configuration\n- Merge API already reports \"CI still pending\" as an error\n- Retries every 30s, up to 20 times (~10 min)\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eWhy per-base-branch concurrency?\u003c/summary\u003e\n\n```yaml\nconcurrency:\n  group: staqd-${{ needs.resolve-base.outputs.base-ref }}\n  cancel-in-progress: false\n```\n\nCommands targeting the same base branch (e.g. `main`) are serialized to prevent race conditions and force-push conflicts. Independent stacks targeting different base branches run in parallel. `cancel-in-progress: false` creates a FIFO queue per base branch.\n\u003c/details\u003e\n\n## Alternatives\n\n| | Staqd | Graphite | ghstack |\n|---|---|---|---|\n| Installation | None (workflow file) | CLI + account | CLI |\n| Stack storage | PR body HTML comments | `.graphite_info` + server | Commit metadata |\n| Restack trigger | PR comment | `gt restack` CLI | `ghstack` CLI |\n| Conflict resolution | Async (Actions → local fix → push) | Sync (terminal) | Sync (terminal) |\n| Tree stacks | Supported (siblings) | Supported (DAG) | Linear only |\n| Merge queue | Comment-based | Dedicated UI | None |\n| External dependency | None | SaaS | None |\n\n## Auto-update on Manual Merge\n\nWhen a parent PR is merged through the GitHub UI (instead of `st merge`), Staqd automatically:\n\n1. **Updates each child PR's base branch** to the parent's base (e.g., `main`)\n2. **Posts a notification comment** on each child PR\n\nThis prevents child PRs from becoming orphaned with a deleted base branch. The child PR may still need rebasing — run `st restack` on the child if needed.\n\n\u003e **Requires** `closed` in your workflow's `pull_request.types`:\n\u003e ```yaml\n\u003e pull_request:\n\u003e   types: [opened, edited, closed]\n\u003e ```\n\n## Troubleshooting\n\n**CI doesn't run after restack push:**\n`GITHUB_TOKEN` pushes don't trigger other workflows. Set up a GitHub App or PAT.\n\n**merge-all stops at a conflict:**\nThe result table shows the merge order and which PRs failed. Fix the conflict locally, push, then run `st merge` on the failed PRs in the order shown (e.g., `Fix the issue and run \\`st merge\\` in order: #3 → #5`).\n\n**merge-all CI timeout:**\nDefault wait is ~10 min (30s × 20 retries). Increase `tryMerge` retry count for slower CI.\n\n**PR diff looks wrong after restack:**\nThe PR's base branch may not have been updated. `stack restack` handles this automatically, but you can manually change the base in PR settings.\n\n**Second PR in merge-all always fails initially:**\nIf branch protection has required checks, CI may not exist yet right after restack. `tryMerge` retries until CI is created and passes.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsiner308%2Fstaqd","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsiner308%2Fstaqd","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsiner308%2Fstaqd/lists"}