When new commits are pushed, the status job and handle job both fire on
the same synchronize event and run in parallel. The event payload's
pr.labels reflect the PRE-push state — so if the PR was approved before
the push, the status job reads 'approved' from the payload and sets
success on the NEW SHA, even though handle is about to (or already did)
remove the label.
Observed in modelcontextprotocol#2375: label removed at 23:06:29, status
set success at 23:06:30 (one second later, from stale payload), merged
at 23:06:34.
Fix: add invalidate-on-push input (default true, mirroring the main
action). On synchronize with invalidation enabled, set pending
unconditionally — don't trust the payload labels. The subsequent
unlabeled event re-evaluates and stays pending.
The previous status-job pattern (inline shell that exits 1 when 'approved'
is missing) shows a red failed check on every fresh PR. GitHub Actions jobs
only have pass/fail — there's no way to express 'pending' via exit code.
The new sub-action uses the commit-status API to set state=pending until
the approved label appears. Hold label produces state=failure. The status
job itself now always succeeds; the commit status it creates is what branch
protection enforces.
Caller workflow swaps the 20-line jq+exit block for a one-line uses: call,
adds statuses:write to permissions, and marks 'slash-commands/approval'
as a required status check (replacing the job-based check).
The previous attempt (constructing orgClient via require('@actions/github')
inside the inline script) fails with MODULE_NOT_FOUND — the package is
bundled into github-script's dist/index.js and not resolvable even from
the eval'd inline script.
Switch to a raw fetch() call for the team membership endpoint. Node 20
(github-script@v8) has global fetch, so this has zero dependencies and
is immune to Octokit's auth-hook override behavior.
@actions/github is only resolvable from inside the github-script bundle,
not from main.js. Build the PAT-authed Octokit client in action.yml and
pass it into main as orgClient. Switch the team-membership check to use
orgClient.rest.teams.getMembershipForUserInOrg instead of a raw request
with header override.
PR #5 broke the action in production — the require() fails with
MODULE_NOT_FOUND because actions/github-script bundles @actions/github
into dist/index.js; it's not in node_modules and not resolvable from
a sibling require()'d file.
Fix: flip the architecture. github-script's 'github' client now uses
bot-token (GITHUB_TOKEN), so the single 'github' object serves all
user-visible operations. The PAT is passed via env (ORG_TOKEN) and
used ONLY for the team-membership call via github.request() with an
explicit authorization header override.
Side effect: PAT requirements shrink further. It now ONLY needs
Organization Members:read — no repo perms, no Actions:write. All
repo-level operations (including workflow dispatch) use GITHUB_TOKEN
with the workflow's permissions: block.
Previously the action used the PAT (SLASH_COMMANDS_TOKEN) for everything,
so comments and reviews showed as authored by whoever owns the PAT. This
is confusing — the PAT exists only because team-membership checks need
read:org, which GITHUB_TOKEN can never have.
New bot-token input (default ${{ github.token }}) is used for all
user-visible side effects. The PAT is now scoped to what actually needs
it: team membership, CODEOWNERS fetch, listFiles, pulls.get, and
workflow dispatch.
Side benefit: the PAT's required scopes shrink. It no longer needs
Pull requests:write or Issues:write — just Contents:read + Members:read
(+ Actions:write for /stageblog). README updated to reflect this.
listFiles pagination + workflow dispatch can take a few seconds. Without
an early ack, the commenter gets no feedback until the outcome reaction
lands. 👀 confirms the command was recognized and authorized while the
heavier work runs.
Installs Go + Hugo extended and builds a Hugo site in preview
(--buildDrafts --buildFuture, development env) or production mode.
Extracted from the MCP repo's blog-preview, deploy-blog, and stage-blog
workflows where the ~12-line Go+Hugo install+build block was duplicated.
Placing it here (rather than as a local .github/actions/ composite in the
MCP repo) means stage-blog.yml can reference it at @main — fork-proof by
construction, no two-checkout dance needed when building fork-submitted
content.
Both deploy and cleanup previously read context.payload.pull_request.number
directly, which throws on workflow_dispatch and other non-PR triggers.
New optional inputs:
- deploy: pr-number (default: event PR number), commit-sha (default: github.sha)
- cleanup: pr-number (default: event PR number)
If pr-number resolves to empty/0, the comment step is skipped gracefully
instead of crashing.
This lets /stageblog's companion workflow_dispatch receiver call the
composite deploy action directly instead of inlining wrangler + noindex
injection. The noindex logic staying in the actions repo (rather than in
a script file inside the caller repo that gets checked out at the fork's
SHA) is also the secure path — fork authors can't tamper with it.
The slash-commands README example is updated to use the deploy action
with the new inputs. The earlier commit on this branch (which rewrote
the example to call wrangler directly) is effectively superseded.
The cloudflare-pages-preview/deploy action reads context.payload.pull_request.number
unconditionally in its comment step, which throws on workflow_dispatch. The
previous example would crash. Updated to call wrangler-action directly and
post the comment manually.
Extends slash-commands with a new /stageblog command that lets authorized
maintainers trigger a workflow_dispatch for staging blog previews from fork
PRs, where the normal pull_request-triggered preview cannot access secrets.
- Captures pr.head.sha at comment time and passes it to the dispatched
workflow so fork authors cannot push new commits between review and build
- Disabled unless stageblog-workflow input is set
- Refuses with stageblog-not-blog result if no changed file matches
stageblog-paths (default blog/**)
- Documents companion workflow pattern, additional PAT scope (Actions:
write), and SHA-pinning safety requirement
Move the handler logic out of the YAML script block into a standalone
main.js loaded via require(GITHUB_ACTION_PATH). This improves
readability and makes the JS easier to lint and unit-test directly.
Prow-style PR approval via comment commands, gated by org team membership
or CODEOWNERS of the PR's changed files.
- /lgtm adds 'approved' label + bot APPROVE review; /lgtm cancel undoes both
- /hold adds 'do-not-merge/hold'; /hold cancel or /unhold removes it
- Self-approval is blocked; unauthorized attempts get a thumbs-down reaction only
- New commits pushed to a PR invalidate approval (label removed, review dismissed)
- CODEOWNERS is fetched from the PR base ref to prevent tampering
Requires a PAT or App token with read:org — default GITHUB_TOKEN cannot
check team membership.
Closesmodelcontextprotocol/modelcontextprotocol#2199
The caller already knows how to wire secrets.X into with: inputs; the table
only needs to name the input, not repeat the secret name in a YAML snippet.
- cloudflare-pages-preview/README.md: secrets table now prescribes exact repo
secret names (CF_PAGES_PREVIEW_API_TOKEN, CF_PAGES_PREVIEW_ACCOUNT_ID) with
the literal passed-as YAML snippet. Usage example updated to match.
- CLAUDE.md: mandate the three-column table format with concrete names, and
require usage examples to reference the same names
- cloudflare-pages-preview/README.md: replace secrets table with per-secret
subsections giving exact CF dashboard navigation for obtaining the API token
(My Profile > API Tokens > Custom Token, Account > Pages > Edit permission)
and account ID (right sidebar on any domain/Workers overview), plus explicit
guidance to store each as a named repo secret
- CLAUDE.md: README secrets section must now document the exact UI path to
obtain each secret, minimum permission scope, input mapping, and storage
location
- cloudflare-pages-preview/README.md: add dedicated Required secrets section
listing the CF API token (with minimum permission scope) and account ID,
which inputs they map to, and a note that github-token needs no extra secret
- CLAUDE.md: make Required secrets a mandatory README section for new actions
- README.md: list actions in a table with separate rows for deploy/cleanup
- CLAUDE.md: conventions for adding new composite actions (layout, shell
requirement, env-var input passing, secrets, github-token, README
requirements, validation)
Two composite actions extracted from modelcontextprotocol's blog-preview workflow:
- deploy: injects noindex headers/meta, deploys to CF Pages under a per-PR
branch alias, posts/updates a sticky PR comment with preview URLs
- cleanup: deletes all CF Pages deployments for the PR branch alias on close,
updates the sticky comment
Callers provide a pre-built static HTML directory; the actions handle
everything Cloudflare-specific.