Skip to content
Closed
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
42 changes: 42 additions & 0 deletions .github/workflows/retract.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Retract Release
on:
issue_comment:
types: [created]
jobs:
retract:
runs-on: ubuntu-latest
name: Retract a release
# not a pull request and has `#contains` on a line by itself
# fromJSON is used to process escape sequences
if: |
!github.event.issue.pull_request &&
contains(
format(fromJSON('"\n{0}\n"'), github.event.comment.body),
fromJSON('"\n#retract\n"')
)
steps:
- name: Get repo contents
uses: actions/checkout@v6
with:
path: .__publish__

- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: 24
cache: yarn
cache-dependency-path: .__publish__/yarn.lock

- name: Install yarn dependencies
run: yarn install --cwd ".__publish__"

- name: Parse and set inputs
id: inputs
run: node .__publish__/src/publish/inputs.js

- name: Comment and close
if: ${{ fromJSON(steps.inputs.outputs.result).requester == github.event.sender.login }}
Copy link

Choose a reason for hiding this comment

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

Missing requester field makes retract always skip

High Severity

The workflow condition checks fromJSON(steps.inputs.outputs.result).requester, but detailsFromContext (invoked by inputs.js) never includes a requester field in its return value — it only returns repo, path, version, dry_run, merge_target, and targets. Since requester is always undefined, the comparison with github.event.sender.login will always be false, causing the "Comment and close" step to be permanently skipped. The #retract command will never actually work.

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

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

Bug: The detailsFromContext function is missing the requester field, causing a conditional step in the retract.yml workflow to always be skipped, breaking the feature.
Severity: CRITICAL

Suggested Fix

Modify the detailsFromContext function in src/modules/details-from-context.js to include the requester field in the object it returns. The value should be populated from the GitHub context, for example, using context.payload.sender.login.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: .github/workflows/retract.yml#L38

Potential issue: The GitHub workflow at `.github/workflows/retract.yml:38` includes a
conditional step that checks `fromJSON(steps.inputs.outputs.result).requester`. However,
the upstream `detailsFromContext` function in `src/modules/details-from-context.js` does
not add a `requester` field to the JSON object it generates. Consequently, the condition
`undefined == github.event.sender.login` always evaluates to false. This causes the
"Comment and close" step to be skipped every time the workflow runs, which breaks the
entire `#retract` feature, as the issue will never be commented on or closed.

Did we get this right? 👍 / 👎 to inform future reviews.

env:
PUBLISH_ARGS: ${{ steps.inputs.outputs.result }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: node .__publish__/src/publish/retract.js
Comment on lines +7 to +42

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}

Copilot Autofix

AI 28 days ago

In general, the fix is to explicitly declare a permissions block specifying the least privileges the workflow needs, either at the top level (applies to all jobs) or within the specific job (retract). This prevents accidental over-privileging via inherited repository/organization defaults and documents the workflow’s intended access.

For this specific workflow, the job checks out the repository (actions/checkout) and runs Node scripts that use GITHUB_TOKEN. At minimum, actions/checkout requires contents: read. The “retract release” / “comment and close” behavior likely needs to write comments and potentially modify issues or releases; however, we should still start from a least-privilege base. A safe minimal baseline that matches the CodeQL recommendation is to add a job-level permissions block with contents: read. If the internal scripts need additional scopes (e.g., issues: write or pull-requests: write, or contents: write for release/tag manipulation), they should be added there; since that code isn’t shown, we won’t assume extra privileges. Concretely, in .github/workflows/retract.yml, under jobs: retract:, insert:

permissions:
  contents: read

indented correctly between the name: and runs-on: (or directly after runs-on:), so the job’s GITHUB_TOKEN is restricted to read-only repository contents unless the repo admin further tightens it.

Suggested changeset 1
.github/workflows/retract.yml

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/.github/workflows/retract.yml b/.github/workflows/retract.yml
--- a/.github/workflows/retract.yml
+++ b/.github/workflows/retract.yml
@@ -6,6 +6,8 @@
   retract:
     runs-on: ubuntu-latest
     name: Retract a release
+    permissions:
+      contents: read
     # not a pull request and has `#contains` on a line by itself
     # fromJSON is used to process escape sequences
     if: |
EOF
@@ -6,6 +6,8 @@
retract:
runs-on: ubuntu-latest
name: Retract a release
permissions:
contents: read
# not a pull request and has `#contains` on a line by itself
# fromJSON is used to process escape sequences
if: |
Copilot is powered by AI and may make mistakes. Always verify output.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ flowchart TD
D --> E["Create issue in getsentry/publish"]
E --> F{Release Manager Review}
F -->|"Add 'accepted' label"| G[Publish workflow triggers]
F -->|"Comment '#retract'"| H[Issue closed - retracted]
G --> I[Download artifacts from GitHub]
I --> J["craft publish to registries"]
J --> K{Publish successful?}
Expand Down Expand Up @@ -80,6 +81,11 @@ jobs:

The same `merge_target` input is also available when using the [Craft composite action](https://craft.sentry.dev/github-actions/#option-2-composite-action) directly.

## Retracting Release Request

To retract a release request, comment `#retract` (as the only comment content) under the request you want to retract.
The only person that can do retract a release, is the same person that initially requested it and is listed in the request description.

## Approvals

Packages we release into the wider world that our customers install, require an explicit approval. This for instance applies to
Expand Down
49 changes: 49 additions & 0 deletions src/modules/__tests__/retract-release.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { vi, describe, test, expect } from "vitest";

vi.mock("fs");

const retractRelease = require("../retract-release.js");

describe("retract release", () => {
test("comment and close issue", async () => {
const retractArgs = {
inputs: { repo: "sentry", version: "21.3.1" },
context: {
repo: { owner: "getsentry", repo: "publish" },
payload: {
sender: { login: "example-user" },
issue: { number: "211" },
},
},
octokit: {
rest: {
issues: {
createComment: vi.fn(),
update: vi.fn(),
},
},
},
};

await retractRelease(retractArgs);

const commentMock = retractArgs.octokit.rest.issues.createComment;
expect(commentMock).toHaveBeenCalledTimes(1);
expect(commentMock.mock.calls[0][0]).toEqual({
body: `Release request retracted by @example-user.
You may also want to remove your [release branch](https://github.com/getsentry/sentry/branches/all?query=21.3.1).`,
issue_number: "211",
owner: "getsentry",
repo: "publish",
});

const updateMock = retractArgs.octokit.rest.issues.update;
expect(updateMock).toHaveBeenCalledTimes(1);
expect(updateMock.mock.calls[0][0]).toEqual({
issue_number: "211",
state: "closed",
owner: "getsentry",
repo: "publish",
});
});
});
2 changes: 2 additions & 0 deletions src/modules/__tests__/update-issue.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ Quick links:
- [View changes](https://github.com/getsentry/sentry-elixir/compare/2f5876adf89822cc75199576966df4fd587f68e9...refs/heads/release/10.7.2)
- [View check runs](https://github.com/getsentry/sentry-elixir/commit/fcbc69b88481a95532d10a6162a243107fabb96a/checks/)
Assign the **accepted** label to this issue to approve the release.
To retract the release, the person requesting it must leave a comment containing \`#retract\` on a line by itself under this issue.

### Targets

Expand All @@ -167,6 +168,7 @@ Quick links:
- [View changes](https://github.com/getsentry/sentry-elixir/compare/2f5876adf89822cc75199576966df4fd587f68e9...refs/heads/release/10.7.2)
- [View check runs](https://github.com/getsentry/sentry-elixir/commit/fcbc69b88481a95532d10a6162a243107fabb96a/checks/)
Assign the **accepted** label to this issue to approve the release.
To retract the release, the person requesting it must leave a comment containing \`#retract\` on a line by itself under this issue.

### Targets

Expand Down
24 changes: 24 additions & 0 deletions src/modules/retract-release.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
async function retract({ context, octokit, inputs }) {
const { repo, version } = inputs;
const { repo: publishRepo } = context;
const { number: issue_number } = context.payload.issue;
const { login } = context.payload.sender;

await Promise.all([
octokit.rest.issues.createComment({
...publishRepo,
issue_number,
body: `Release request retracted by @${login}.\nYou may also want to remove your [release branch](https://github.com/getsentry/${repo}/branches/all?query=${encodeURIComponent(
version
)}).`,
}),

octokit.rest.issues.update({
...publishRepo,
issue_number,
state: "closed",
}),
]);
};

module.exports = retract;
9 changes: 9 additions & 0 deletions src/publish/retract.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const github = require('@actions/github');
const retract = require('../modules/retract-release');
const {getGitHubToken} = require('../libs/github');

const context = github.context;
const octokit = github.getOctokit(getGitHubToken());
const inputs = JSON.parse(process.env.PUBLISH_ARGS);

retract({context, octokit, inputs});