# sticky-pull-request-comment keeps one comment alive instead of adding a new one each run

> marocchino/sticky-pull-request-comment is a MIT licensed GitHub Action, written in TypeScript, that creates a comment on a pull request and updates that same comment on later runs, with headers for keeping several independent comments, and with options to append, recreate, delete, hide or hide and recreate. The interesting failure mode is not the action but GitHub's default workflow permissions, which it explains in detail.

**marocchino/sticky-pull-request-comment** — create comment on pull request, if exists update that comment.

- Repository: https://github.com/marocchino/sticky-pull-request-comment
- Stars: 656 · Forks: 102
- Language: TypeScript
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/marocchino-sticky-pull-request-comment

## The unit of work is a comment that survives the next run

The action does one small thing, which is why the README is short and the input list is long. It creates a comment on a pull request, and if that comment already exists it updates it instead of adding another. The stated problem is the one every CI setup runs into. A workflow that posts a build status on every push produces a pull request covered in comments, each one a slightly different record, and a reviewer cannot tell which is current. This action inverts that. The comment becomes a slot rather than an event, and the slot is keyed by a header so that different parts of a pipeline can each own one. The library runs with GitHub Actions, and the README even suggests reading the GitHub Actions documentation first if the example grammar does not look friendly enough, which is an honest hint about the learning curve of the action syntax itself.

## The permission block is the part everyone gets wrong

The action needs write access to pull requests, and the README documents the failure that follows when it does not have it. The error is Resource not accessible by integration, and the explanation is that the tool requires write permission and that message means the requester does not have enough permission. The cause, for a new repository, is that GitHub sets permissions conservatively for newly created repositories, and the fix is in Settings, then Actions, then General, then Workflow permissions, where read and write permissions have to be enabled. Once that is done, the job needs the permission declared:

```yaml
permissions:
  pull-requests: write
```

There is a second trap for private repositories. Setting this setting removes all default permissions on your workflow, so for the actions/checkout step to work, which the README calls highly common, you also need the contents permission, or you get an error such as fatal: repository not found:

```yaml
permissions:
  pull-requests: write
  contents: read
```

That detail is the difference between an action that works and one that fails on the first run in a private repository, and it is the most useful thing in the documentation.

## The header is the key that keeps comments independent

The header input is what makes the action usable in a pipeline with more than one status to report. It is optional, it is not shown to users, and it determines if the comment should be updated. The README is explicit that it can be used when you want to add multiple comments independently to a given object. So the model is: a pull request plus a header identifies a comment, and a run updates the comment with that header on that pull request, creating it if it is not there. In practice that lets a release job and a test job write to the same pull request without fighting over one comment. The simplest usage, with no header at all, posts a single comment that every subsequent run overwrites:

```yaml
uses: marocchino/sticky-pull-request-comment@v3
with:
  message: |
    Release ${{ github.sha }} to <https://pr-${{ github.event.number }}.example.com>
```

## Append, recreate, delete and hide are four different lifecycles

The input list is where the design becomes clear, because the options are not variations on one behaviour but four distinct lifecycles. append indicates that new comment messages should be appended to the previous comment message, and the previous comment is kept. recreate indicates that the previous comment should be removed before a new one is created. delete removes a previously created comment, with the header used to point at which one. hide hides a previously created comment, again with the header identifying it, and hide_classify chooses the reason, with SPAM, ABUSE, OFF_TOPIC, OUTDATED, DUPLICATE and RESOLVED available and OUTDATED as the default. hide_and_recreate combines hiding with a fresh comment, and hide_details hides the summary tags on the previously created comment. Two priority rules are worth memorising. only_create creates a comment only if there is no existing one and otherwise does nothing, and it has higher priority than hide_and_recreate and hide. only_update is the mirror image, updating an existing comment and doing nothing otherwise.

## The body can come from a file, and a template can hold the file

Writing a long message inline in a workflow file is unpleasant, so the action accepts a path to a file containing the comment message. The simplest form is:

```yaml
uses: marocchino/sticky-pull-request-comment@v3
with:
  path: path-to-comment-contents.txt
```

When the message and the path are used together, the message can carry a placeholder. Use {{{content}}} as a placeholder in the message to insert file content at that position, which lets a pipeline run a command, write its output to a file, and drop that output into a fenced block inside a comment:

```yaml
- name: Run Test
  run: rake test > result.txt
- uses: marocchino/sticky-pull-request-comment@v3
  with:
    path: result.txt
    message: |
      ```
      {{{content}}}
      ```
```

The three brace form is deliberate, because the double brace syntax is the one GitHub Actions reserves for expressions, so a plain curly pair would be swallowed by the expression parser. A workflow that appends test output to an existing comment is a few lines further, and the same triple brace placeholder works there.

## Commenting on a pull request that did not trigger the run

Two inputs exist for the case where the workflow runs on something other than a pull request event. The first is to find the pull request yourself and pass its number, using a separate action to locate it:

```yaml
- uses: jwalton/gh-find-current-pr@v1
  id: finder
- uses: marocchino/sticky-pull-request-comment@v3
  with:
    number: ${{ steps.finder.outputs.pr }}
    message: |
      Test ${{ github.sha }} ended successfully.
      This message is from a push.
```

The README frames this as the escape hatch for when triggering on pr is not possible. The second is number_force, which takes a literal pull request number and posts there regardless of which pull request triggered the workflow, which is how one workflow can be pointed at a tracking issue or a parent pull request. Both inputs assume the same underlying rule, that the action needs a pull request number to act on and will otherwise have nothing to comment on.

## A TypeScript action bundled with rollup

The repository shows a modern action layout. There is an action.yml at the root, the source in src/, the compiled output committed under dist/, tests in __tests__/, and the configuration for biome, rollup and vitest alongside a tsconfig.json and a .tool-versions file. The package manifest gives the runtime dependencies as @actions/core, @actions/github, @actions/glob and the Octokit GraphQL schema package, and the scripts cover the usual chain of tsc for the build, biome for format and lint, rollup for the bundle, and vitest for the tests, with an all script that runs the whole sequence. The version in the manifest is 3.0.4 while the published releases are v3.0.5, v3.0.4 and v3.0.3, and the last push was on 2026-10-19, so the project is current and the manifest number lags the tag by a patch, which is normal for a bundled action whose dist output is committed.

## The alternative is posting the comment yourself

The alternative to this action is a plain workflow step that calls the API to create a comment, for example through the GitHub CLI or a curl to the pull request comments endpoint. The difference in approach is entirely about lifecycle. A manual step has no memory, so it cannot find the comment it wrote last time, and the result is a new comment on every run, which is the exact problem this action exists to solve. You can approximate the behaviour with a marker in the comment body and a search, but then you own the parsing, the update, the deletion and the hiding, and every pipeline that needs a status comment ends up with a slightly different copy of that logic. This action is that logic in one place, with the header, append, recreate, delete and hide options already implemented and the permission handling documented. If you only ever post one comment once, the manual step is simpler, and if you need the comment to be edited in place across many runs, the action is the cheaper route.

## Conclusion

Adopt sticky-pull-request-comment if your workflow posts a status comment on every pull request and the pile of near-duplicate comments is the problem, since the action finds its own comment and updates it, and can append to it, hide it, or replace it, using a header as the key. Do not adopt it for a single-use comment or for a workflow that must run on a fork pull request with restricted tokens, because the action needs write permission on pull requests and cannot post where the token cannot write. Before you wire it in, set the permissions block explicitly, add contents: read if the repository is private, and read the input list carefully, since several options have priority relationships that are easy to misread.

## FAQ

### How do I use sticky-pull-request-comment in a GitHub Actions workflow?

Reference the action with uses: marocchino/sticky-pull-request-comment@v3, give it a message, and grant the job the pull-requests: write permission. The action creates the comment on the pull request if it does not exist and updates that same comment on later runs.

### What does the header option do in sticky-pull-request-comment?

The header determines if the comment should be updated and is not shown to users. It lets you add multiple comments independently to the same pull request, so different steps in a pipeline can each maintain their own comment.

### Why do I get Resource not accessible by integration?

The action requires write permission on pull requests, so the requester does not have enough permission. For a newly created repository you have to enable read and write permissions under Settings, Actions, General, Workflow permissions, and then set permissions: pull-requests: write on the job, adding contents: read if the repository is private.

### How do I post a comment on a pull request when the workflow runs on a push?

Find the pull request with a separate action and pass its number, as in the README example that uses jwalton/gh-find-current-pr@v1 with the id finder and then number: ${{ steps.finder.outputs.pr }}. The number_force input takes a literal number if you want to post to a specific pull request regardless of the triggering event.

### What is the difference between recreate, delete, hide and append in sticky-pull-request-comment?

They are four separate lifecycles. append keeps the previous comment and adds to it, recreate removes the previous comment and creates a new one, delete removes a previously created comment and hide hides it with a classification from SPAM, ABUSE, OFF_TOPIC, OUTDATED, DUPLICATE or RESOLVED, with OUTDATED as the default. only_create and only_update act as guards and take priority over hide_and_recreate and hide.

## Sources

- [Issues](https://github.com/marocchino/sticky-pull-request-comment/issues)
- [License: MIT](https://github.com/marocchino/sticky-pull-request-comment/blob/main/LICENSE)
- [marocchino/sticky-pull-request-comment on GitHub](https://github.com/marocchino/sticky-pull-request-comment)
- [README](https://github.com/marocchino/sticky-pull-request-comment/blob/main/README.md)
- [Releases](https://github.com/marocchino/sticky-pull-request-comment/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/marocchino-sticky-pull-request-comment
