Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 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
13 changes: 6 additions & 7 deletions .agents/skills/fix-security-vulnerability/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,14 @@ make # installs yarn deps AND runs 'pre-commit install'
pre-commit install
```

The pre-commit config (`.pre-commit-config.yaml`) runs formatters, linters, and a `set-docker-tag-from-branch` hook that rewrites `action.yml` so the Docker tag matches the current branch name. **This is not optional.** CI's `prepare-docker` job rejects any PR whose `action.yml` Docker tag still matches a semver like `3.6.0`. The pre-commit hook is what keeps the tag in sync on feature branches; CI does **not** install or run pre-commit itself.
The pre-commit config (`.pre-commit-config.yaml`) runs formatters and linters. CI does **not** install or run pre-commit itself.

### Hook-modified files: re-stage, re-commit (never `--amend`)

On the first `git commit`, the `set-docker-tag-from-branch` hook will usually modify `action.yml`. The commit fails, and the hook's changes are left unstaged. Recover by:
If a hook modifies files, the commit fails and the hook's changes are left unstaged. Recover by:

```bash
git add action.yml
git add <modified files>
git commit -m "<same message>" # a NEW commit; do not use --amend
```

Expand Down Expand Up @@ -123,10 +123,9 @@ Co-Authored-By: <agent model name> <noreply@anthropic.com>
EOF
)"

# 4. If the 'set-docker-tag-from-branch' hook rewrote action.yml (it usually
# does on the first commit of a branch), the commit above fails. Re-stage
# and re-commit — DO NOT use --amend.
git add action.yml
# 4. If a pre-commit hook modified files, the commit above fails. Re-stage
# and re-commit. DO NOT use --amend.
git add <modified files>
git commit -m "<same message as above>"
```

Expand Down
6 changes: 2 additions & 4 deletions .agents/skills/verify-dist/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,13 @@ Regenerates dist/index.js.

Whoever runs the follow-up commit needs the repo's pre-commit hooks installed. Verify with `ls .git/hooks/pre-commit`; if missing, run `make` (installs yarn deps + `pre-commit install`) or `pre-commit install`.

The `set-docker-tag-from-branch` hook will rewrite `action.yml` on the first commit of a branch and fail the commit. Recover by staging `action.yml` and committing again (**never `--amend`** — the hook-failed commit never landed):
If a hook modifies files, the commit fails. Recover by staging the modified files and committing again (**never `--amend`**, because the hook-failed commit never landed):

```bash
git add action.yml
git add <modified files>
git commit -m "<same message>"
```

CI does **not** run pre-commit. CI's `prepare-docker` job rejects PRs where `action.yml`'s Docker tag still matches a semver like `3.6.0`, so keeping the tag in sync via the local hook is what prevents the CI failure.

## Notes

- The CI check runs `git diff --ignore-space-at-eol dist/` — whitespace-only diffs pass, anything else fails. Match that locally.
Expand Down
57 changes: 57 additions & 0 deletions .github/actions/use-local-image/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: 'Use local action image'
description: |-
Loads the action image built in this workflow run into a local registry
and points the checked-out action.yml to it. The e2e tests use this, so
they can run before any image goes to ghcr.io (and on fork PRs, which get
a read-only token).

inputs:
path:
description: Directory of the checked-out action that the tests run.
default: '.'
required: false

runs:
using: 'composite'
steps:
- name: Download image
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: action-release-image-${{ runner.arch == 'ARM64' && 'arm64' || 'amd64' }}
# Relative path, because `runner.temp` is a host path in container jobs.
path: .action-release-image

# Container jobs have the host docker socket mounted but usually no docker CLI.
- name: Install docker CLI
shell: bash
run: |
if command -v docker &> /dev/null; then
exit 0
fi
curl -fsSL "https://download.docker.com/linux/static/stable/$(uname -m)/docker-29.9.1.tgz" \
| tar -xz -C /usr/local/bin --strip-components=1 docker/docker

# The runner always pulls `docker://` images, so a local copy is not enough.
# The daemon trusts registries on localhost without TLS.
- name: Push image to local registry
shell: bash
run: |
docker run -d --rm -p 5000:5000 --name action-release-registry \
registry:3.1.2@sha256:d4032d4cc323c1731c707931ecada07a53076eac38e3b61ae57533f170f2c6c3
docker load -i .action-release-image/image.tar
rm -rf .action-release-image
# Retry until the registry is ready. A curl check does not work in
# container jobs, because there localhost is the job container.
for i in $(seq 1 10); do
docker push localhost:5000/action-release-image:pr && exit 0
sleep 2
done
exit 1

- name: Point action.yml to local image
shell: bash
working-directory: ${{ inputs.path }}
run: |
sed -i -e 's|docker://ghcr.io/getsentry/action-release-image:.*|docker://localhost:5000/action-release-image:pr|' action.yml
# Make sure the replacement worked
grep -q 'docker://localhost:5000/action-release-image:pr' action.yml
111 changes: 83 additions & 28 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ env:
jobs:
prepare-docker:
name: Prepare docker tag
if: github.event_name == 'push'
runs-on: ubuntu-latest
permissions:
contents: write
Expand All @@ -41,14 +42,6 @@ jobs:
else
TAG=$(yq '... | select(has("uses") and .uses | test("docker://ghcr.io/getsentry/action-release-image:.*")) | .uses' action.yml | awk -F':' '{print $3}')
echo "docker_tag=$TAG" >> $GITHUB_OUTPUT

if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then
if [[ "$TAG" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: docker_tag $TAG matching format MAJOR.MINOR.PATCH is not allowed inside pull requests."
echo "Please rename the docker tag in action.yml and try again."
exit 1
fi
fi
fi

- name: Get auth token
Expand All @@ -66,12 +59,14 @@ jobs:
github-token: ${{ steps.token.outputs.token }}
message: 'chore: Set docker tag for master [skip ci]'

docker-build-platform:
name: Build & publish Docker images
needs: prepare-docker
# Builds the images without a push, so this job needs no write access. PRs
# (including fork PRs) and pushes use the same build. The e2e tests load the
# app image into a local registry, see `use-local-image`.
docker-build:
name: Build Docker images
runs-on: ${{ matrix.platform.runner }}
permissions:
packages: write
contents: read
strategy:
matrix:
target:
Expand All @@ -90,39 +85,52 @@ jobs:
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
fetch-depth: 0
Comment on lines -93 to -94

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This was a leftover from when the step created a sentry release that tested set_commits and is no longer needed.


- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3

- name: Login to GitHub Container Registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

# BUILDKIT_INLINE_CACHE creates the image in such a way that you can
# then use --cache-from (think of a remote cache)
# This feature is allowed thanks to using the buildx plugin
#
# There's a COPY command in the builder stage that can easily invalidate the cache
# If you notice, please add more exceptions to .dockerignore since we loose the value
# of using --cache-from on the app stage
- name: Build and push
- name: Build
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with:
platforms: ${{ matrix.platform.name }}
push: true
tags: ghcr.io/${{ github.repository_owner }}/${{ matrix.target.image }}:${{ needs.prepare-docker.outputs.docker_tag }}-${{ matrix.platform.arch }}
cache-from: ghcr.io/${{ github.repository_owner }}/${{ matrix.target.image }}:master
tags: localhost:5000/${{ matrix.target.image }}:pr
outputs: type=docker,dest=${{ runner.temp }}/image.tar
# Inline cache only holds the final stage, so the app build also
# reads the builder image to reuse the builder stage.
cache-from: |
ghcr.io/${{ github.repository_owner }}/action-release-builder-image:master
ghcr.io/${{ github.repository_owner }}/action-release-image:master
target: ${{ matrix.target.name }}
build-args: BUILDKIT_INLINE_CACHE=1

docker-build:
name: Merge multi-arch Docker images
needs: [prepare-docker, docker-build-platform]
- name: Upload image
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: ${{ matrix.target.image }}-${{ matrix.platform.arch }}
path: ${{ runner.temp }}/image.tar
retention-days: 1

# The only job that can write to ghcr.io. It runs only on push (reviewed code),
# after the e2e tests pass, and publishes the exact images they tested.
docker-publish:
name: Publish Docker images
needs:
- prepare-docker
- docker-build
- test-create-staging-release-per-push
- test-runs-on-container
- test-mock-release
- test-mock-release-working-directory
- test-node-version-preserved
- test-manual-commit-range
if: github.event_name == 'push'
runs-on: ubuntu-latest
permissions:
packages: write
Expand All @@ -132,13 +140,30 @@ jobs:
- action-release-builder-image
- action-release-image
steps:
- name: Download images
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
pattern: ${{ matrix.image }}-*
path: ${{ runner.temp }}/images

- name: Login to GitHub Container Registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Push arch images
env:
LOCAL_IMAGE: localhost:5000/${{ matrix.image }}:pr
IMAGE: ghcr.io/${{ github.repository_owner }}/${{ matrix.image }}:${{ needs.prepare-docker.outputs.docker_tag }}
run: |
for ARCH in amd64 arm64; do
docker load -i "$RUNNER_TEMP/images/${{ matrix.image }}-$ARCH/image.tar"
docker tag "$LOCAL_IMAGE" "$IMAGE-$ARCH"
docker push "$IMAGE-$ARCH"
done

- name: Create and verify multi-arch manifest
env:
IMAGE: ghcr.io/${{ github.repository_owner }}/${{ matrix.image }}:${{ needs.prepare-docker.outputs.docker_tag }}
Expand Down Expand Up @@ -175,6 +200,8 @@ jobs:

test-create-staging-release-per-push:
needs: docker-build
# Needs SENTRY_AUTH_TOKEN, which fork PRs do not get.
if: ${{ !github.event.pull_request.head.repo.fork }}
strategy:
matrix:
os: [ubuntu-latest, ubuntu-24.04-arm, windows-latest, macos-latest]
Expand All @@ -187,6 +214,10 @@ jobs:
with:
fetch-depth: 0

- name: Use local action image
if: runner.os == 'Linux'
uses: ./.github/actions/use-local-image

- name: Create a staging release
uses: ./
env:
Expand All @@ -197,6 +228,8 @@ jobs:

test-runs-on-container:
needs: docker-build
# Needs SENTRY_AUTH_TOKEN, which fork PRs do not get.
if: ${{ !github.event.pull_request.head.repo.fork }}
runs-on: ubuntu-latest
permissions:
contents: read
Expand All @@ -208,6 +241,10 @@ jobs:
with:
fetch-depth: 0

- name: Use local action image
if: runner.os == 'Linux'
uses: ./.github/actions/use-local-image

- name: Create a staging release
uses: ./
env:
Expand All @@ -230,6 +267,10 @@ jobs:
with:
fetch-depth: 0

- name: Use local action image
if: runner.os == 'Linux'
uses: ./.github/actions/use-local-image

- name: Mock creating a Sentry release
uses: ./
env:
Expand Down Expand Up @@ -259,6 +300,12 @@ jobs:
fetch-depth: 0
path: test/

- name: Use local action image
if: runner.os == 'Linux'
uses: ./main/.github/actions/use-local-image
with:
path: main

- name: Mock creating a Sentry release in a different directory
uses: ./main
env:
Expand All @@ -282,6 +329,10 @@ jobs:
with:
fetch-depth: 0

- name: Use local action image
if: runner.os == 'Linux'
uses: ./.github/actions/use-local-image

- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
Expand Down Expand Up @@ -330,6 +381,10 @@ jobs:
with:
fetch-depth: 0

- name: Use local action image
if: runner.os == 'Linux'
uses: ./.github/actions/use-local-image

- name: Create a release with manual commit range
uses: ./
env:
Expand Down
5 changes: 0 additions & 5 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,3 @@ repos:
entry: yarn lint
language: system
pass_filenames: false
- id: set-docker-tag-from-branch
name: Set docker tag in action.yml from current git branch
entry: yarn set-docker-tag-from-branch
language: system
pass_filenames: false
2 changes: 1 addition & 1 deletion dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -100637,7 +100637,7 @@ const BROWSER_PAGELOAD_SPAN_OP="pageload",BROWSER_NAVIGATION_SPAN_OP="navigation
/***/ ((module) => {

"use strict";
module.exports = JSON.parse('{"name":"action-release","version":"3.7.2","private":true,"description":"GitHub Action for creating a release on Sentry","main":"dist/index.js","scripts":{"all":"yarn run format && yarn run lint && yarn run build && yarn test","build":"ncc build src/main.ts -e @sentry/cli","format":"prettier --write **/*.ts **/*.md","format-check":"prettier --check **/*.ts **/*.md","lint":"eslint src/**/*.ts","set-docker-tag":"./scripts/set-docker-tag.sh","set-docker-tag-from-branch":"./scripts/set-docker-tag-from-branch.sh","start":"node dist/index.js","test":"jest"},"repository":{"type":"git","url":"git+https://github.com/getsentry/action-release.git"},"keywords":["actions","sentry","release"],"author":"Sentry","license":"MIT","dependencies":{"@actions/core":"^2.0.3","@sentry/cli":"^2.58.6","@sentry/node":"^10.71.0"},"devDependencies":{"@types/jest":"^29.5.6","@types/node":"^24.0.0","@typescript-eslint/parser":"^6.9.0","@vercel/ncc":"^0.38.1","eslint":"^8.52.0","eslint-plugin-github":"^4.10.1","eslint-plugin-jest":"^27.4.3","jest":"^29.7.0","jest-circus":"^29.7.0","js-yaml":"^4.3.2","prettier":"^3.0.3","ts-jest":"^29.1.1","typescript":"^5.2.2"},"resolutions":{"**/@istanbuljs/load-nyc-config/js-yaml":"3.15.2"},"volta":{"node":"24.22.1","yarn":"1.22.4"}}');
module.exports = JSON.parse('{"name":"action-release","version":"3.7.2","private":true,"description":"GitHub Action for creating a release on Sentry","main":"dist/index.js","scripts":{"all":"yarn run format && yarn run lint && yarn run build && yarn test","build":"ncc build src/main.ts -e @sentry/cli","format":"prettier --write **/*.ts **/*.md","format-check":"prettier --check **/*.ts **/*.md","lint":"eslint src/**/*.ts","set-docker-tag":"./scripts/set-docker-tag.sh","start":"node dist/index.js","test":"jest"},"repository":{"type":"git","url":"git+https://github.com/getsentry/action-release.git"},"keywords":["actions","sentry","release"],"author":"Sentry","license":"MIT","dependencies":{"@actions/core":"^2.0.3","@sentry/cli":"^2.58.6","@sentry/node":"^10.71.0"},"devDependencies":{"@types/jest":"^29.5.6","@types/node":"^24.0.0","@typescript-eslint/parser":"^6.9.0","@vercel/ncc":"^0.38.1","eslint":"^8.52.0","eslint-plugin-github":"^4.10.1","eslint-plugin-jest":"^27.4.3","jest":"^29.7.0","jest-circus":"^29.7.0","js-yaml":"^4.3.2","prettier":"^3.0.3","ts-jest":"^29.1.1","typescript":"^5.2.2"},"resolutions":{"**/@istanbuljs/load-nyc-config/js-yaml":"3.15.2"},"volta":{"node":"24.22.1","yarn":"1.22.4"}}');

/***/ })

Expand Down
18 changes: 9 additions & 9 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,15 @@ Telemetry for internal development is collected using `@sentry/node`, see `src/t
Members of this repo will not have to set anything up since [the integration](https://sentry-ecosystem.sentry.io/settings/developer-settings/end-to-end-action-release-integration-416eb2/) is already set-up. Just open the PR and you will see [a release created](https://sentry-ecosystem.sentry.io/releases/?project=4505075304693760) for your PR.

1. Create a branch
2. Run `yarn set-docker-tag-from-branch` to set a docker tag based on your github branch name. This is important so that the action gets its own Docker image, allowing you to test the action in a different repo.
3. Make changes
4. If possible, add unit and E2E tests (inside `.github/workflows/build.yml`)
5. Run `yarn install` to install deps
6. Run `yarn build` to build the action
7. Commit the changes and the build inside `dist/`

If you forget to run `yarn set-docker-tag-from-branch` the repo's pre-commit hooks will do it for you and fail the commit.
Just add the changes to staging and commit again.
2. Make changes
3. If possible, add unit and E2E tests (inside `.github/workflows/build.yml`)
4. Run `yarn install` to install deps
5. Run `yarn build` to build the action
6. Commit the changes and the build inside `dist/`

Do not change the Docker image tag in `action.yml`. PR builds do not publish an image to ghcr.io.
The E2E tests load the image of the PR into a local registry (see `.github/actions/use-local-image`).
To use your changes from a different repo, merge them and use `getsentry/action-release@master`.

## Testing

Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
"format-check": "prettier --check **/*.ts **/*.md",
"lint": "eslint src/**/*.ts",
"set-docker-tag": "./scripts/set-docker-tag.sh",
"set-docker-tag-from-branch": "./scripts/set-docker-tag-from-branch.sh",
"start": "node dist/index.js",
"test": "jest"
},
Expand Down
10 changes: 0 additions & 10 deletions scripts/set-docker-tag-from-branch.sh

This file was deleted.

Loading