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
2 changes: 2 additions & 0 deletions .agents/skills/ci/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -436,3 +436,5 @@ Locally:
**Auto-release:** `.github/workflows/auto-release.yml` fires on push to `main`. It diffs the two version-bearing files (`CMakeLists.txt` project version, `.claude-plugin/plugin.json` version) against the previous push range and creates the corresponding `v<x.y.z>` or `plugin-v<x.y.z>` tag. The existing tag-triggered release workflows (`release-cli.yml`, `sign-and-release.yml`) then build and publish. `Release: skip reason="..."` on the merging commit suppresses the tag.

**Tag safety:** the auto-release workflow is idempotent-strict — if a tag already exists pointing at a different SHA, it fails loudly rather than overwriting. See `docs/guides/versioning.md` for the manual recovery recipe.

**`RELEASE_BOT_TOKEN` is required for the auto-release chain to fire.** Without it, auto-release silently degrades — tags get created via `GITHUB_TOKEN` but GitHub doesn't trigger workflows on `GITHUB_TOKEN`-pushed tags, so `release-cli.yml` and `sign-and-release.yml` never run and no GitHub Release appears. Run `pulp doctor` to check; if missing, follow the "One-time setup" section in `docs/guides/versioning.md`. `pulp pr` will also print a heads-up before pushing the PR if the secret isn't present.
30 changes: 30 additions & 0 deletions docs/guides/versioning.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,36 @@ Never type `gh pr create` + `shipyard ship` separately. Never run the version-bu

---

## One-time setup: `RELEASE_BOT_TOKEN` secret

The auto-release workflow needs a fine-grained PAT to push tags so that the tag-triggered binary workflows (`release-cli.yml`, `sign-and-release.yml`) actually fire. **Without this secret, auto-release silently degrades**: tags are still created via `GITHUB_TOKEN`, but GitHub Actions deliberately does not chain workflows from `GITHUB_TOKEN`-pushed tags (anti-infinite-loop safety), so the binary release workflows never run and no GitHub Release appears.

Run `pulp doctor` to check whether the secret is configured. If it shows `RELEASE_BOT_TOKEN secret — missing`, set it up:

1. **Generate the token.** github.com → top-right avatar → Settings → Developer settings → Personal access tokens → **Fine-grained tokens** → Generate new token.
2. **Token name:** `pulp-release-bot` (or any descriptive name).
3. **Expiration:** 1 year (mark your calendar to renew).
4. **Resource owner:** the org or user that owns this repo.
5. **Repository access:** Only select repositories → this repo only.
6. **Permissions** (Repository permissions section): **Contents: Read and write**. Leave everything else at the default.
7. **Generate**, copy the token (starts with `github_pat_…`).
8. **Add to repo secrets:** github.com/&lt;owner&gt;/&lt;repo&gt;/settings/secrets/actions → New repository secret. Name: `RELEASE_BOT_TOKEN`. Value: paste the token.

That's it — no code change needed. The workflow already reads `${{ secrets.RELEASE_BOT_TOKEN || secrets.GITHUB_TOKEN }}`. `pulp doctor` will then report `RELEASE_BOT_TOKEN secret — configured ...`. `pulp pr` will also stop printing the heads-up warning before each push.

### Manual fallback when the secret isn't set

The chain still works but requires one manual step per release after the auto-tag appears:

```bash
gh workflow run release-cli.yml --ref v<x.y.z>
gh workflow run sign-and-release.yml --ref v<x.y.z>
```

(Pulp's first auto-released tag, `v0.4.0`, used this fallback before `RELEASE_BOT_TOKEN` was provisioned.)

---

## Agent parity

Both Claude Code and Codex pick up this policy from `CLAUDE.md`. Codex reads `AGENTS.md` which is a thin pointer at `CLAUDE.md` — the single source of truth for both agents. There is no separate policy file for Codex, and `AGENTS.md` intentionally stays empty so the two never drift.
Expand Down
46 changes: 46 additions & 0 deletions tools/cli/cli_common.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1573,6 +1573,52 @@ std::vector<DoctorCheck> run_doctor_checks(const fs::path& active_root, bool sta
}
}

// Release-bot token check (best-effort, repo-aware).
// The auto-release workflow falls back to GITHUB_TOKEN when
// RELEASE_BOT_TOKEN isn't set, but tags pushed by GITHUB_TOKEN
// don't trigger downstream workflows (GitHub anti-infinite-loop
// safety), so the binary release pipeline silently never fires.
// Surfacing this in doctor is the cheapest way to keep contributors
// out of that trap. Skipped silently when:
// - we can't detect the GitHub repo (not a checkout, no remote)
// - `gh` is unavailable or unauthenticated
// - the user lacks `actions:read` for the repo
// because none of those mean the user did anything wrong; the
// existing `gh` row already reports the gh tool's health.
{
auto repo_slug = first_line(exec_output(
"gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null"));
if (!repo_slug.empty() && repo_slug.find('/') != std::string::npos) {
auto secrets_list = exec_output(
"gh api 'repos/" + repo_slug + "/actions/secrets' "
"--jq '.secrets[].name' 2>/dev/null");
// Only emit a check if we got a usable response.
if (!secrets_list.empty()) {
DoctorCheck c{"RELEASE_BOT_TOKEN secret", false, {}, {}};
Comment on lines +1595 to +1597

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Treat empty secrets response as missing token in doctor

This check only emits a DoctorCheck when secrets_list is non-empty, but --jq '.secrets[].name' returns an empty string when a repository has zero Actions secrets. In that common bootstrap case, pulp doctor will skip the RELEASE_BOT_TOKEN row entirely instead of reporting it as missing, so contributors still hit the silent post-merge release failure this change is meant to prevent.

Useful? React with 👍 / 👎.

bool present = secrets_list.find("RELEASE_BOT_TOKEN") != std::string::npos;
if (present) {
c.passed = true;
c.detail = "configured on " + repo_slug
+ " — auto-release tags will trigger release-cli.yml + sign-and-release.yml";
} else {
c.detail = "missing on " + repo_slug
+ " — auto-release tags will fall back to GITHUB_TOKEN, "
"which does NOT trigger the binary release workflows";
c.fix =
"Create a fine-grained PAT and store as RELEASE_BOT_TOKEN:\n"
" 1. github.com -> Settings -> Developer settings -> Personal access tokens\n"
" -> Fine-grained tokens -> Generate new token\n"
" 2. Repo access: only " + repo_slug + "\n"
" 3. Permission: Contents = Read and write\n"
" 4. github.com/" + repo_slug + "/settings/secrets/actions\n"
" -> New repository secret named RELEASE_BOT_TOKEN\n"
" See docs/guides/versioning.md for the full walkthrough.";
}
checks.push_back(c);
}
}
}

return checks;
}

Expand Down
28 changes: 27 additions & 1 deletion tools/cli/cmd_pr.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,10 @@ int run_passthrough(const std::string& cmd) {

// ── Helpers ─────────────────────────────────────────────────────────────

std::string trim(const std::string& s) {
// Local trim — defined as static so it doesn't collide with the
// equivalent non-static definition in cli_common.cpp. Both have the same
// behavior; we keep the local one to avoid forcing a header rebuild.
static std::string trim(const std::string& s) {
size_t a = s.find_first_not_of(" \t\r\n");
if (a == std::string::npos) return {};
size_t b = s.find_last_not_of(" \t\r\n");
Expand Down Expand Up @@ -274,6 +277,29 @@ int cmd_pr(const std::vector<std::string>& args) {
return 0;
}

// Best-effort heads-up: warn if RELEASE_BOT_TOKEN is missing on the
// active GitHub repo. Doesn't block — gates and merge are unaffected
// — but surfacing the trap here means the user finds out before they
// wonder why the post-merge GitHub Release never appeared.
{
auto repo = ::trim(run_capture("gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null").stdout_text);
if (!repo.empty() && repo.find('/') != std::string::npos) {
auto secrets = run_capture(
"gh api 'repos/" + repo + "/actions/secrets' "
"--jq '.secrets[].name' 2>/dev/null").stdout_text;
if (!secrets.empty() && secrets.find("RELEASE_BOT_TOKEN") == std::string::npos) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Warn in pulp pr even when the secrets list is empty

The warning path requires !secrets.empty(), so pulp pr does not print any heads-up when a repo has zero Actions secrets even though RELEASE_BOT_TOKEN is definitely absent. That suppresses the new preflight warning in exactly the setup scenario where users need it most, and they only discover the problem after merge when release workflows do not fire.

Useful? React with 👍 / 👎.

std::cerr << color::yellow()
<< "\n▸ Heads-up: RELEASE_BOT_TOKEN secret is missing on "
<< repo << ".\n"
<< " Auto-release will tag the version bump but "
"the binary release workflows won't fire.\n"
<< " Run `pulp doctor` for the one-time setup steps "
"(or see docs/guides/versioning.md).\n"
<< color::reset();
}
}
}

// Step 1: skill-sync — must pass before any file is rewritten.
if (int rc = step_skill_sync(root, opt.base); rc != 0) {
std::cerr << "\npulp pr: skill-sync gate failed. Update the listed SKILL.md(s)\n"
Expand Down
Loading