├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── maintainer-blank.yml │ ├── 01-feature.yml │ └── 02-bug_report.yml └── workflows │ ├── verify-dist.yml │ ├── create-release-tags.yml │ ├── release.yml │ └── build.yml ├── .eslintignore ├── .prettierignore ├── entrypoint.sh ├── images └── prepare_release.png ├── Makefile ├── .craft.yml ├── jest.config.js ├── CONTRIBUTING.md ├── scripts ├── set-docker-tag-from-branch.sh ├── set-docker-tag.sh └── craft-pre-release.sh ├── .prettierrc.json ├── .dockerignore ├── docs ├── publishing-a-release.md └── development.md ├── .pre-commit-config.yaml ├── src ├── cli.ts ├── telemetry.ts ├── main.ts └── options.ts ├── tsconfig.json ├── Dockerfile ├── .gitignore ├── package.json ├── .eslintrc.json ├── CHANGELOG.md ├── action.yml ├── __tests__ └── main.test.ts ├── LICENSE └── README.md /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @andreiborza 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | node_modules/ -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | node_modules/ -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -l 2 | node /action-release/dist/index.js -------------------------------------------------------------------------------- /images/prepare_release.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getsentry/action-release/HEAD/images/prepare_release.png -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | install: setup-git 2 | yarn install 3 | 4 | setup-git: 5 | ifneq (, $(shell which pre-commit)) 6 | pre-commit install 7 | endif 8 | -------------------------------------------------------------------------------- /.craft.yml: -------------------------------------------------------------------------------- 1 | minVersion: 0.23.1 2 | changelogPolicy: auto 3 | preReleaseCommand: bash scripts/craft-pre-release.sh 4 | artifactProvider: 5 | name: none 6 | targets: 7 | - name: github 8 | tagPrefix: v -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Join Sentry Discord 4 | url: https://discord.com/invite/sentry 5 | about: A place to talk about SDK development and other Sentry related topics. It's not meant as a support channel. 6 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | moduleFileExtensions: ['js', 'ts'], 4 | testEnvironment: 'node', 5 | testMatch: ['**/*.test.ts'], 6 | testRunner: 'jest-circus/runner', 7 | transform: { 8 | '^.+\\.ts$': 'ts-jest' 9 | }, 10 | verbose: true 11 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | See the [development guide](./docs/development.md) for getting started developing this action. 4 | 5 | Feel free to use GitHub's pull request features to propose changes. 6 | 7 | See the [Code of Conduct](https://github.com/getsentry/.github/blob/main/CODE_OF_CONDUCT.md). 8 | -------------------------------------------------------------------------------- /scripts/set-docker-tag-from-branch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eux 3 | 4 | # Extract the branch name from git and replace all non-alphanumerical characters with `-` 5 | BRANCH=$(git rev-parse --abbrev-ref HEAD | sed 's/[^a-zA-Z0-9-]/-/g') 6 | 7 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 8 | cd $SCRIPT_DIR 9 | 10 | ./set-docker-tag.sh $BRANCH 11 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "es5", 8 | "bracketSpacing": true, 9 | "arrowParens": "avoid", 10 | "overrides": [ 11 | { 12 | "files": "*.md", 13 | "options": { 14 | "proseWrap": "preserve" 15 | } 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Docs: https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 | # These files will be ignore by Docker for COPY and ADD commands when creating a build context 3 | # In other words, if a file should not be inside of the Docker container it should be 4 | # added to the list, otherwise, it will invalidate cache layers and have to rebuild 5 | # all layers after a COPY command 6 | .git 7 | .github 8 | Dockerfile 9 | .dockerignore 10 | *.md -------------------------------------------------------------------------------- /scripts/set-docker-tag.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eux 3 | 4 | DOCKER_REGISTRY_IMAGE="docker://ghcr.io/getsentry/action-release-image" 5 | TAG="${1}" 6 | 7 | # Move to the project root 8 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 9 | cd $SCRIPT_DIR/.. 10 | 11 | # We don't want the backup but this is the only way to make this 12 | # work on macos as well 13 | sed -i.bak -e "s|\($DOCKER_REGISTRY_IMAGE:\)[^']*|\1$TAG|" action.yml && rm -f action.yml.bak 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/maintainer-blank.yml: -------------------------------------------------------------------------------- 1 | name: Blank Issue 2 | description: Blank Issue. Reserved for maintainers. 3 | body: 4 | - type: textarea 5 | id: description 6 | attributes: 7 | label: Description 8 | description: Please describe the issue. 9 | validations: 10 | required: true 11 | 12 | - type: markdown 13 | attributes: 14 | value: |- 15 | ## Thanks 🙏 16 | Check our [triage docs](https://open.sentry.io/triage/) for what to expect next. 17 | -------------------------------------------------------------------------------- /scripts/craft-pre-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eux 3 | 4 | OLD_VERSION="${1}" 5 | NEW_VERSION="${2}" 6 | 7 | # Move to the project root 8 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 9 | cd $SCRIPT_DIR/.. 10 | 11 | # Bump the npm version without creating a git tag for it 12 | # as craft/publish will take care of that 13 | export npm_config_git_tag_version=false 14 | npm version "${NEW_VERSION}" 15 | 16 | # The build output contains the package.json so we need to 17 | # rebuild to ensure it's reflected after bumping the version 18 | yarn install && yarn build 19 | 20 | # Update the docker tag in action.yml 21 | yarn set-docker-tag "${NEW_VERSION}" 22 | -------------------------------------------------------------------------------- /docs/publishing-a-release.md: -------------------------------------------------------------------------------- 1 | # Publishing a Release 2 | 3 | _These steps are only relevant to Sentry employees when preparing and publishing a new release._ 4 | 5 | 1. Open the [Prepare Release workflow](https://github.com/getsentry/sentry-javascript/actions/workflows/release.yml) and 6 | fill in ![Prepare a Release workflow](../images/prepare_release.png) 7 | 1. Click gray `Run workflow` button 8 | 2. Fill in the version you want to release, e.g. `1.13.3` 9 | 3. Click the green `Run workflow` button 10 | 2. A new issue should appear in https://github.com/getsentry/publish/issues. 11 | 3. Wait until the CI check runs have finished successfully (there is a link to them in the issue). 12 | 4. Once CI passes successfully, ask a member of the 13 | [@getsentry/releases-approvers](https://github.com/orgs/getsentry/teams/release-approvers) to approve the release. 14 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v5.0.0 6 | hooks: 7 | - id: trailing-whitespace 8 | exclude: '^dist/.*' 9 | - id: end-of-file-fixer 10 | exclude: '^dist/.*' 11 | - id: check-yaml 12 | - repo: local 13 | hooks: 14 | - id: format 15 | name: Format 16 | entry: yarn format 17 | language: system 18 | pass_filenames: false 19 | - id: lint 20 | name: Lint 21 | entry: yarn lint 22 | language: system 23 | pass_filenames: false 24 | - id: set-docker-tag-from-branch 25 | name: Set docker tag in action.yml from current git branch 26 | entry: yarn set-docker-tag-from-branch 27 | language: system 28 | pass_filenames: false 29 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | import { getTraceData } from '@sentry/node'; 2 | import SentryCli, { SentryCliReleases } from '@sentry/cli'; 3 | // @ts-ignore 4 | import { version } from '../package.json'; 5 | 6 | /** 7 | * CLI Singleton 8 | * 9 | * When the `MOCK` environment variable is set, stub out network calls. 10 | */ 11 | export const getCLI = (): SentryCliReleases => { 12 | // Set the User-Agent string. 13 | process.env['SENTRY_PIPELINE'] = `github-action-release/${version}`; 14 | 15 | const cli = new SentryCli(null, { 16 | headers: { 17 | // Propagate sentry trace if we have one 18 | ...getTraceData(), 19 | }, 20 | }).releases; 21 | 22 | if (process.env['MOCK']) { 23 | cli.execute = async ( 24 | args: string[], 25 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 26 | live: boolean 27 | ): Promise => { 28 | return Promise.resolve(args.join(' ')); 29 | }; 30 | } 31 | 32 | return cli; 33 | }; 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/01-feature.yml: -------------------------------------------------------------------------------- 1 | name: 💡 Feature Request 2 | description: Tell us about a problem our GitHub action could solve but doesn't. 3 | labels: ["Feature"] 4 | body: 5 | - type: textarea 6 | id: problem 7 | attributes: 8 | label: Problem Statement 9 | description: What problem could Sentry solve that it doesn't? 10 | placeholder: |- 11 | I want to make whirled peas, but Sentry doesn't blend. 12 | validations: 13 | required: true 14 | 15 | - type: textarea 16 | id: expected 17 | attributes: 18 | label: Solution Brainstorm 19 | description: We know you have bright ideas to share ... share away, friend. 20 | placeholder: |- 21 | Add a blender to Sentry. 22 | validations: 23 | required: false 24 | 25 | - type: markdown 26 | attributes: 27 | value: |- 28 | ## Thanks 🙏 29 | Check our [triage docs](https://open.sentry.io/triage/) for what to expect next. 30 | -------------------------------------------------------------------------------- /.github/workflows/verify-dist.yml: -------------------------------------------------------------------------------- 1 | # Inspired by @action/checkout 2 | # 3 | # Compares the checked in `dist/index.js` with the PR's `dist/index.js` 4 | # On mismatch this action fails 5 | name: Verify dist 6 | 7 | on: 8 | push: 9 | branches: 10 | - master 11 | - release/** 12 | paths-ignore: 13 | - "**.md" 14 | pull_request: 15 | paths-ignore: 16 | - "**.md" 17 | 18 | jobs: 19 | check-dist: 20 | name: Verify dist/index.js file 21 | 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | with: 27 | fetch-depth: 0 28 | 29 | - name: Use volta 30 | uses: volta-cli/action@v4 31 | 32 | - name: Install dependencies 33 | run: yarn install 34 | 35 | - name: Rebuild dist 36 | run: yarn build 37 | 38 | - name: Compare expected and actual dist 39 | run: | 40 | if [ "$(git diff --ignore-space-at-eol dist/ | wc -l)" -gt "0" ]; then 41 | echo "Detected uncommitted changes after build. Did you forget to commit `dist/index.js`?" 42 | echo "Diff:" 43 | git diff 44 | exit 1 45 | fi 46 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 4 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 5 | "outDir": "./lib", /* Redirect output structure to the directory. */ 6 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 7 | "strict": true, /* Enable all strict type-checking options. */ 8 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 9 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 10 | "resolveJsonModule": true /* Enables importing JSON files. */ 11 | }, 12 | "exclude": ["node_modules", "**/*.test.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/create-release-tags.yml: -------------------------------------------------------------------------------- 1 | # Releases always publish a `v[major].[minor].[patch]` tag 2 | # This action creates/updates `v[major]` and `v[major].[minor]` tags 3 | name: Create release tags 4 | 5 | on: 6 | release: 7 | types: [released] 8 | 9 | permissions: 10 | contents: write 11 | 12 | jobs: 13 | create-tags: 14 | name: Create release tags for major and minor versions 15 | 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | ref: ${{ github.event.release.tag_name }} 22 | 23 | - name: Set git user to getsentry-bot 24 | run: | 25 | echo "GIT_COMMITTER_NAME=getsentry-bot" >> $GITHUB_ENV; 26 | echo "GIT_AUTHOR_NAME=getsentry-bot" >> $GITHUB_ENV; 27 | echo "EMAIL=bot@sentry.io" >> $GITHUB_ENV; 28 | 29 | - name: Create and push major and minor version tags 30 | run: | 31 | MAJOR_VERSION=$(echo '${{ github.event.release.tag_name }}' | cut -d. -f1) 32 | MINOR_VERSION=$(echo '${{ github.event.release.tag_name }}' | cut -d. -f1-2) 33 | git tag -f $MAJOR_VERSION 34 | git tag -f $MINOR_VERSION 35 | git push -f origin $MAJOR_VERSION $MINOR_VERSION 36 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # The multi stage set up *saves* up image size by avoiding the dev dependencies 2 | # required to produce dist/ 3 | FROM node:20-alpine AS builder 4 | WORKDIR /app 5 | # This layer will invalidate upon new dependencies 6 | COPY package.json yarn.lock ./ 7 | RUN export YARN_CACHE_FOLDER="$(mktemp -d)" \ 8 | && yarn install --frozen-lockfile --quiet \ 9 | && rm -r "$YARN_CACHE_FOLDER" 10 | # If there's some code changes that causes this layer to 11 | # invalidate but it shouldn't, use .dockerignore to exclude it 12 | COPY . . 13 | RUN yarn build 14 | 15 | FROM node:20-alpine AS app 16 | COPY package.json yarn.lock /action-release/ 17 | # On the builder image, we install both types of dependencies rather than 18 | # just the production ones. This generates /action-release/node_modules 19 | RUN export YARN_CACHE_FOLDER="$(mktemp -d)" \ 20 | && cd /action-release \ 21 | && yarn install --frozen-lockfile --production --quiet \ 22 | && rm -r "$YARN_CACHE_FOLDER" 23 | 24 | # Copy the artifacts from `yarn build` 25 | COPY --from=builder /app/dist /action-release/dist/ 26 | RUN chmod +x /action-release/dist/index.js 27 | 28 | RUN printf '[safe]\n directory = *\n' > /etc/gitconfig 29 | 30 | COPY entrypoint.sh /entrypoint.sh 31 | RUN chmod +x /entrypoint.sh 32 | ENTRYPOINT ["/entrypoint.sh"] 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directory 2 | node_modules 3 | 4 | # Rest pulled from https://github.com/github/gitignore/blob/master/Node.gitignore 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # node-waf configuration 33 | .lock-wscript 34 | 35 | # Compiled binary addons (https://nodejs.org/api/addons.html) 36 | build/Release 37 | 38 | # Dependency directories 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # TypeScript cache 45 | *.tsbuildinfo 46 | 47 | # Optional npm cache directory 48 | .npm 49 | 50 | # Optional eslint cache 51 | .eslintcache 52 | 53 | # Optional REPL history 54 | .node_repl_history 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # OS metadata 60 | .DS_Store 61 | Thumbs.db 62 | 63 | # Ignore built ts files 64 | __tests__/runner/* 65 | lib/**/* 66 | 67 | # IDE settings 68 | .idea/ 69 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: 'Action: Prepare Release' 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: Version to release 8 | required: true 9 | force: 10 | description: Force a release even when there are release-blockers (optional) 11 | required: false 12 | merge_target: 13 | description: Target branch to merge into. Uses the default branch as a fallback (optional) 14 | required: false 15 | default: master 16 | 17 | jobs: 18 | release: 19 | name: Release a new version 20 | 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - name: Get auth token 25 | id: token 26 | uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0 27 | with: 28 | app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }} 29 | private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }} 30 | 31 | - uses: actions/checkout@v4 32 | with: 33 | token: ${{ steps.token.outputs.token }} 34 | fetch-depth: 0 35 | 36 | - name: Prepare release 37 | uses: getsentry/action-prepare-release@v1 38 | env: 39 | GITHUB_TOKEN: ${{ steps.token.outputs.token }} 40 | with: 41 | version: ${{ github.event.inputs.version }} 42 | force: ${{ github.event.inputs.force }} 43 | merge_target: ${{ github.event.inputs.merge_target }} 44 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/02-bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 🐞 Bug Report 2 | description: Tell us about something that's not working the way we (probably) intend. 3 | labels: ["Bug"] 4 | body: 5 | - type: textarea 6 | id: environment 7 | attributes: 8 | label: Environment 9 | description: How do you use this action? 10 | placeholder: |- 11 | Standard Github runners or self-hosted runners (which OS and arch?) 12 | validations: 13 | required: true 14 | 15 | - type: input 16 | id: version 17 | attributes: 18 | description: Which version of the action do you use? 19 | placeholder: "v1 ← should look like this" 20 | label: Version 21 | validations: 22 | required: true 23 | 24 | - type: textarea 25 | id: repro 26 | attributes: 27 | label: Steps to Reproduce 28 | description: How can we see what you're seeing? Specific is terrific. 29 | placeholder: |- 30 | 1. foo 31 | 2. bar 32 | 3. baz 33 | validations: 34 | required: true 35 | 36 | - type: textarea 37 | id: expected 38 | attributes: 39 | label: Expected Result 40 | validations: 41 | required: true 42 | 43 | - type: textarea 44 | id: actual 45 | attributes: 46 | label: Actual Result 47 | description: Logs? Screenshots? Yes, please. 48 | validations: 49 | required: true 50 | 51 | - type: markdown 52 | attributes: 53 | value: |- 54 | ## Thanks 🙏 55 | Check our [triage docs](https://open.sentry.io/triage/) for what to expect next. 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "action-release", 3 | "version": "3.4.0", 4 | "private": true, 5 | "description": "GitHub Action for creating a release on Sentry", 6 | "main": "dist/index.js", 7 | "scripts": { 8 | "all": "yarn run format && yarn run lint && yarn run build && yarn test", 9 | "build": "ncc build src/main.ts -e @sentry/cli", 10 | "format": "prettier --write **/*.ts **/*.md", 11 | "format-check": "prettier --check **/*.ts **/*.md", 12 | "lint": "eslint src/**/*.ts", 13 | "set-docker-tag": "./scripts/set-docker-tag.sh", 14 | "set-docker-tag-from-branch": "./scripts/set-docker-tag-from-branch.sh", 15 | "start": "node dist/index.js", 16 | "test": "jest" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/getsentry/action-release.git" 21 | }, 22 | "keywords": [ 23 | "actions", 24 | "sentry", 25 | "release" 26 | ], 27 | "author": "Sentry", 28 | "license": "MIT", 29 | "dependencies": { 30 | "@actions/core": "^1.11.1", 31 | "@sentry/node": "^8.54.0", 32 | "@sentry/cli": "^2.41.1" 33 | }, 34 | "devDependencies": { 35 | "@types/jest": "^29.5.6", 36 | "@types/node": "^20.8.9", 37 | "@typescript-eslint/parser": "^6.9.0", 38 | "@vercel/ncc": "^0.38.1", 39 | "eslint": "^8.52.0", 40 | "eslint-plugin-github": "^4.10.1", 41 | "eslint-plugin-jest": "^27.4.3", 42 | "jest": "^29.7.0", 43 | "jest-circus": "^29.7.0", 44 | "js-yaml": "^4.1.0", 45 | "prettier": "^3.0.3", 46 | "ts-jest": "^29.1.1", 47 | "typescript": "^5.2.2" 48 | }, 49 | "volta": { 50 | "node": "20.19.2", 51 | "yarn": "1.22.4" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["jest", "@typescript-eslint"], 3 | "extends": ["plugin:github/recommended"], 4 | "parser": "@typescript-eslint/parser", 5 | "parserOptions": { 6 | "ecmaVersion": 9, 7 | "sourceType": "module", 8 | "project": "./tsconfig.json" 9 | }, 10 | "rules": { 11 | "eslint-comments/no-use": "off", 12 | "import/no-namespace": "off", 13 | "no-unused-vars": "off", 14 | "@typescript-eslint/no-unused-vars": "error", 15 | "@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}], 16 | "@typescript-eslint/no-require-imports": "error", 17 | "@typescript-eslint/array-type": "error", 18 | "@typescript-eslint/await-thenable": "error", 19 | "camelcase": "off", 20 | "@typescript-eslint/explicit-function-return-type": ["error", {"allowExpressions": true}], 21 | "@typescript-eslint/func-call-spacing": ["error", "never"], 22 | "@typescript-eslint/no-array-constructor": "error", 23 | "@typescript-eslint/no-empty-interface": "error", 24 | "@typescript-eslint/no-explicit-any": "error", 25 | "@typescript-eslint/no-extraneous-class": "error", 26 | "@typescript-eslint/no-for-in-array": "error", 27 | "@typescript-eslint/no-inferrable-types": "error", 28 | "@typescript-eslint/no-misused-new": "error", 29 | "@typescript-eslint/no-namespace": "error", 30 | "@typescript-eslint/no-non-null-assertion": "warn", 31 | "@typescript-eslint/no-unnecessary-qualifier": "error", 32 | "@typescript-eslint/no-unnecessary-type-assertion": "error", 33 | "@typescript-eslint/no-useless-constructor": "error", 34 | "@typescript-eslint/no-var-requires": "error", 35 | "@typescript-eslint/prefer-for-of": "warn", 36 | "@typescript-eslint/prefer-function-type": "warn", 37 | "@typescript-eslint/prefer-includes": "error", 38 | "@typescript-eslint/prefer-string-starts-ends-with": "error", 39 | "@typescript-eslint/promise-function-async": "error", 40 | "@typescript-eslint/require-array-sort-compare": "error", 41 | "@typescript-eslint/restrict-plus-operands": "error", 42 | "@typescript-eslint/type-annotation-spacing": "error", 43 | "@typescript-eslint/unbound-method": "error", 44 | "github/array-foreach": "off", 45 | "i18n-text/no-en": "off" 46 | }, 47 | "env": { 48 | "node": true, 49 | "es6": true, 50 | "jest/globals": true 51 | } 52 | } -------------------------------------------------------------------------------- /src/telemetry.ts: -------------------------------------------------------------------------------- 1 | import * as ciOptions from './options'; 2 | import * as Sentry from '@sentry/node'; 3 | import packageJson from '../package.json'; 4 | 5 | const SENTRY_SAAS_HOSTNAME = 'sentry.io'; 6 | 7 | /** 8 | * Initializes Sentry and wraps the given callback 9 | * in a span. 10 | */ 11 | export async function withTelemetry(options: { enabled: boolean }, callback: () => F | Promise): Promise { 12 | Sentry.initWithoutDefaultIntegrations({ 13 | dsn: 'https://2172f0c14072ba401de59317df8ded93@o1.ingest.us.sentry.io/4508608809533441', 14 | enabled: options.enabled, 15 | environment: `production-sentry-github-action`, 16 | tracesSampleRate: 1, 17 | sampleRate: 1, 18 | release: packageJson.version, 19 | integrations: [Sentry.httpIntegration()], 20 | tracePropagationTargets: ['sentry.io/api'], 21 | }); 22 | 23 | const session = Sentry.startSession(); 24 | 25 | const org = process.env['SENTRY_ORG']; 26 | 27 | Sentry.setUser({ id: org }); 28 | Sentry.setTag('organization', org); 29 | Sentry.setTag('node', process.version); 30 | Sentry.setTag('platform', process.platform); 31 | 32 | try { 33 | return await Sentry.startSpan( 34 | { 35 | name: 'sentry-github-action-execution', 36 | op: 'action.flow', 37 | }, 38 | async () => { 39 | updateProgress('start'); 40 | const res = await callback(); 41 | updateProgress('finished'); 42 | 43 | return res; 44 | } 45 | ); 46 | } catch (e) { 47 | session.status = 'crashed'; 48 | Sentry.captureException('Error during sentry-github-action execution.'); 49 | throw e; 50 | } finally { 51 | Sentry.endSession(); 52 | await safeFlush(); 53 | } 54 | } 55 | 56 | /** 57 | * Sets the `progress` tag to a given step. 58 | */ 59 | export function updateProgress(step: string): void { 60 | Sentry.setTag('progress', step); 61 | } 62 | 63 | /** 64 | * Wraps the given callback in a span. 65 | */ 66 | export function traceStep(step: string, callback: () => T): T { 67 | updateProgress(step); 68 | return Sentry.startSpan({ name: step, op: 'action.step' }, () => callback()); 69 | } 70 | 71 | /** 72 | * Flushing can fail, we never want to crash because of telemetry. 73 | */ 74 | export async function safeFlush(): Promise { 75 | try { 76 | await Sentry.flush(3000); 77 | } catch { 78 | // Noop when flushing fails. 79 | // We don't even need to log anything because there's likely nothing the user can do and they likely will not care. 80 | } 81 | } 82 | 83 | /** 84 | * Check whether the user is self-hosting Sentry. 85 | */ 86 | export function isSelfHosted(): boolean { 87 | const url = new URL(process.env['SENTRY_URL'] || `https://${SENTRY_SAAS_HOSTNAME}`); 88 | 89 | return url.hostname !== SENTRY_SAAS_HOSTNAME; 90 | } 91 | 92 | /** 93 | * Determine if telemetry should be enabled. 94 | */ 95 | export function isTelemetryEnabled(): boolean { 96 | return !ciOptions.getBooleanOption('disable_telemetry', false) && !isSelfHosted(); 97 | } 98 | -------------------------------------------------------------------------------- /docs/development.md: -------------------------------------------------------------------------------- 1 | # Pre-requirements 2 | 3 | This setup assumes you have [Yarn][Yarn], [Volta][volta] and [pre-commit][pre-commit] installed. 4 | 5 | After cloning the repo, run 6 | 7 | ```bash 8 | # Install or update application dependencies 9 | make 10 | ``` 11 | 12 | # Development of `getsentry/action-release` 13 | 14 | This document aims to provide guidelines for maintainers and contains information on how to develop and test this action. 15 | For info on how to release changes, follow [publishing-a-release](publishing-a-release.md). 16 | 17 | The action is a composite GitHub Action. 18 | 19 | On Linux runners, the action executes the underlying JavaScript script 20 | via a Docker image we publish. 21 | 22 | For Mac OS and Windows runners, the action cannot run the Docker image and instead installs 23 | the `@sentry/cli` dependency corresponding to the architecture of the runner and then executes the underlying JavaScript 24 | distribution. 25 | 26 | This split in architecture is done to optimize the run-time of the action but at the same time support non-Linux runners. 27 | 28 | This action runs fastest on Linux runners. 29 | 30 | ## Development 31 | 32 | The action is using `@sentry/cli` under the hood and is written in TypeScript. See `src/main.ts` to get started. 33 | 34 | Options to the action are exposed via `action.yml`, changes that impact options need to be documented in the `README.md`. 35 | 36 | > [!NOTE] 37 | > Actions have to be exposed in 3 places in `action.yml` 38 | > 39 | > 1. Under the `inputs` field - These are the actual inputs exposed to users 40 | > 2. Under the `env` field inside the `Run docker image` step. All inputs have to be mapped from inputs to `INPUT_X` env variables. 41 | > 3. Under the `env` field inside the `Run Release Action 42 | 43 | Telemetry for internal development is collected using `@sentry/node`, see `src/telemetry.ts` for utilities. 44 | 45 | ## Development steps 46 | 47 | > [!NOTE] 48 | > Contributors will need to create an internal integration in their Sentry org and need to be an admin. 49 | > See [#Prerequisites](../README.md#prerequisites). 50 | 51 | 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. 52 | 53 | 1. Create a branch 54 | 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. 55 | 3. Make changes 56 | 4. If possible, add unit and E2E tests (inside `.github/workflows/build.yml`) 57 | 5. Run `yarn install` to install deps 58 | 6. Run `yarn build` to build the action 59 | 7. Commit the changes and the build inside `dist/` 60 | 61 | 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. 62 | Just add the changes to staging and commit again. 63 | 64 | ## Testing 65 | 66 | You can run unit tests with `yarn test`. 67 | 68 | ### Troubleshooting 69 | 70 | - If the `verify-dist` action fails for your PR, you likely forgot to commit the build output. 71 | Run `yarn install` and `yarn build` and commit the `dist/` folder. 72 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 3.4.0 4 | 5 | - feat: Add support for setting manual commit range (#291) by @andreiborza 6 | 7 | Work in this release was contributed by @trevorkwhite. Thank you for your contribution! 8 | 9 | ## 3.3.0 10 | 11 | ### Various fixes & improvements 12 | 13 | - chore: pin cache action (#290) by @saibotk 14 | - chore: Set docker tag for master [skip ci] (ae1d1cd5) by @getsantry[bot] 15 | 16 | Work in this release was contributed by @saibotk. Thank you for your contribution! 17 | 18 | ## 3.2.0 19 | 20 | ### Various fixes & improvements 21 | 22 | - chore: Set docker tag for master [skip ci] (e8340952) by @getsantry[bot] 23 | - feat: Bump to node 20.19.2 (#284) by @andreiborza 24 | - chore: Set docker tag for master [skip ci] (ec695e24) by @getsantry[bot] 25 | 26 | ## 3.1.2 27 | 28 | - fix: Preserve existing Node version on macOS and Windows runners (#280) by @andreiborza 29 | 30 | ## 3.1.1 31 | 32 | - fix: Only pass `urlPrefix` to sentry-cli if it's not empty (#275) by @andreiborza 33 | 34 | ## 3.1.0 35 | 36 | - feat: Add `release` and `release_prefix` in favor of `version` and `version_prefix` (#273) by @andreiborza 37 | 38 | Input parameter `version` has been deprecated and will be removed in a future version in favor of a newly introduced `release` parameter. 39 | 40 | Input parameter `version_prefix` has been deprecated and will be removed in a future version in favor of a newly introduced `release_prefix` parameter. 41 | 42 | ## 3.0.0 43 | 44 | Version `3.0.0` contains breaking changes: 45 | 46 | - feat(sourcemaps)!: Enable injecting debug ids by default (#272) by @andreiborza 47 | 48 | The action now automatically injects Debug IDs into your JavaScript source files and source maps to ensure your stacktraces can be 49 | properly un-minified. 50 | 51 | This is a **breaking change as it modifies your source files**. You can disable this behavior by setting `inject: false`: 52 | 53 | ```yaml 54 | - uses: getsentry/action-release@v3 55 | with: 56 | environment: 'production' 57 | sourcemaps: './dist' 58 | inject: false 59 | ``` 60 | 61 | Read more about [Artifact Bundles and Debug IDs here](https://docs.sentry.io/platforms/javascript/sourcemaps/troubleshooting_js/artifact-bundles/). 62 | 63 | ## 1.11.0 64 | 65 | - feat: Use hybrid docker/composite action approach (#265) by @andreiborza 66 | 67 | After receiving user feedback both on runtime and compatibility issues for `1.10.0` 68 | the action has been reworked to use a Docker based approach on Linux runners, mimicking 69 | `< 1.9.0` versions, while Mac OS and Windows runners will follow the `1.10.0` approach 70 | of installing `@sentry/cli` in the run step. 71 | 72 | ## 1.10.5 73 | 74 | ### Various fixes & improvements 75 | 76 | - fix: Mark `GITHUB_WORKSPACE` a safe git directory (#260) by @andreiborza 77 | 78 | ## 2.0.0 79 | 80 | > [!NOTE] 81 | > This release contains no changes over `v1.10.4` and is just meant to unblock users that have upgraded to `v2` before. 82 | > 83 | > We **recommend** pinning to `v1`. 84 | 85 | Last week we pushed a `v2` branch that triggered dependabot which treated it as a release. 86 | This was not meant to be a release, but many users have upgraded to `v2`. 87 | 88 | This release will help unblock users that have upgraded to `v2`. 89 | 90 | Please see: #209 91 | 92 | ## 1.10.4 93 | 94 | ### Various fixes & improvements 95 | 96 | - fix(action): Use `action/setup-node` instead of unofficial volta action (#256) by @andreiborza 97 | 98 | ## 1.10.3 99 | 100 | ### Various fixes & improvements 101 | 102 | - fix(ci): Use volta to ensure node and npm are available (#255) by @andreiborza 103 | 104 | ## 1.10.2 105 | 106 | - fix(action): Ensure working directory always starts out at repo root (#250) 107 | - fix(action): Use `npm` instead of `yarn` to install `sentry-cli` (#251) 108 | 109 | ## 1.10.1 110 | 111 | This release contains changes concerning maintainers of the repo and has no user-facing changes. 112 | 113 | ## 1.10.0 114 | 115 | - **feat(action): Support macos and windows runners** 116 | We now publish a composite action that runs on all runners. Actions can now be properly versioned, allowing pinning versions from here on out. 117 | 118 | ## 1.9.0 119 | 120 | **Important Changes** 121 | 122 | - **feat(sourcemaps): Add inject option to inject debug ids into source files and sourcemaps (#229)** 123 | A new option to inject Debug IDs into source files and sourcemaps was added to the action to ensure proper un-minifaction of your stacktraces. We strongly recommend enabling this by setting inject: true in your action alongside providing a path to sourcemaps. 124 | 125 | **Other Changes** 126 | 127 | - feat(telemetry): Collect project specific tags (#228) 128 | 129 | ## Previous Releases 130 | 131 | For previous releases, check the [Github Releases](https://github.com/getsentry/action-release/releases) page. 132 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/node'; 2 | import * as core from '@actions/core'; 3 | import { SentryCliUploadSourceMapsOptions } from '@sentry/cli'; 4 | import { getCLI } from './cli'; 5 | import * as options from './options'; 6 | import * as process from 'process'; 7 | import { isTelemetryEnabled, traceStep, withTelemetry } from './telemetry'; 8 | 9 | withTelemetry( 10 | { 11 | enabled: isTelemetryEnabled(), 12 | }, 13 | async () => { 14 | try { 15 | const workingDirectory = options.getWorkingDirectory(); 16 | const currentWorkingDirectory = process.cwd(); 17 | 18 | if (workingDirectory) { 19 | process.chdir(workingDirectory); 20 | } 21 | 22 | // Validate options first so we can fail early. 23 | options.checkEnvironmentVariables(); 24 | 25 | const environment = options.getEnvironment(); 26 | const inject = options.getBooleanOption('inject', true); 27 | const sourcemaps = options.getSourcemaps(); 28 | const dist = options.getDist(); 29 | const shouldFinalize = options.getBooleanOption('finalize', true); 30 | const ignoreMissing = options.getBooleanOption('ignore_missing', false); 31 | const ignoreEmpty = options.getBooleanOption('ignore_empty', false); 32 | const deployStartedAtOption = options.getStartedAt(); 33 | const setCommitsOption = options.getSetCommitsOption(); 34 | const projects = options.getProjects(); 35 | const urlPrefix = options.getUrlPrefixOption(); 36 | const stripCommonPrefix = options.getBooleanOption('strip_common_prefix', false); 37 | const release = await options.getRelease(); 38 | 39 | if (projects.length === 1) { 40 | Sentry.setTag('project', projects[0]); 41 | } else { 42 | Sentry.setTag('projects', projects.join(',')); 43 | } 44 | 45 | core.debug(`Release version is ${release}`); 46 | await getCLI().new(release, { projects }); 47 | 48 | Sentry.setTag('set-commits', setCommitsOption); 49 | 50 | if (setCommitsOption !== 'skip') { 51 | await traceStep('set-commits', async () => { 52 | core.debug(`Setting commits with option '${setCommitsOption}'`); 53 | 54 | if (setCommitsOption === 'auto') { 55 | await getCLI().setCommits(release, { 56 | auto: true, 57 | ignoreMissing, 58 | ignoreEmpty, 59 | }); 60 | } else if (setCommitsOption === 'manual') { 61 | const { repo, commit, previousCommit } = options.getSetCommitsManualOptions(); 62 | 63 | if (!repo || !commit) { 64 | throw new Error('Options `repo` and `commit` are required when `set_commits` is `manual`'); 65 | } 66 | 67 | await getCLI().setCommits(release, { 68 | auto: false, 69 | repo, 70 | commit, 71 | ...(previousCommit && { previousCommit }), 72 | }); 73 | } 74 | }); 75 | } 76 | 77 | Sentry.setTag('sourcemaps', sourcemaps.length > 0); 78 | Sentry.setTag('inject', inject); 79 | 80 | if (sourcemaps.length) { 81 | if (inject) { 82 | await traceStep('inject-debug-ids', async () => { 83 | core.debug(`Injecting Debug IDs`); 84 | // Unfortunately, @sentry/cli does not yet have an alias for inject 85 | await getCLI().execute(['sourcemaps', 'inject', ...sourcemaps], true); 86 | }); 87 | } 88 | 89 | await traceStep('upload-sourcemaps', async () => { 90 | core.debug(`Adding sourcemaps`); 91 | const sourceMapsOptions: SentryCliUploadSourceMapsOptions = { 92 | include: sourcemaps, 93 | dist, 94 | stripCommonPrefix, 95 | }; 96 | 97 | // only set the urlPrefix if it's not empty 98 | if (urlPrefix) { 99 | sourceMapsOptions.urlPrefix = urlPrefix; 100 | } 101 | 102 | // sentry-cli supports multiple projects, but only uploads sourcemaps for the 103 | // first project so we need to upload sourcemaps for each project individually 104 | await Promise.all( 105 | projects.map(async (project: string) => 106 | getCLI().uploadSourceMaps(release, { 107 | ...sourceMapsOptions, 108 | projects: [project], 109 | } as SentryCliUploadSourceMapsOptions & { projects: string[] }) 110 | ) 111 | ); 112 | 113 | Sentry.setTag('sourcemaps-uploaded', true); 114 | }); 115 | } 116 | 117 | if (environment) { 118 | await traceStep('add-environment', async () => { 119 | core.debug(`Adding deploy to release`); 120 | await getCLI().newDeploy(release, { 121 | env: environment, 122 | ...(deployStartedAtOption && { started: deployStartedAtOption }), 123 | }); 124 | }); 125 | } 126 | 127 | Sentry.setTag('finalize', shouldFinalize); 128 | 129 | if (shouldFinalize) { 130 | await traceStep('finalizing-release', async () => { 131 | core.debug(`Finalizing the release`); 132 | await getCLI().finalize(release); 133 | 134 | Sentry.setTag('finalized', true); 135 | }); 136 | } 137 | 138 | if (workingDirectory) { 139 | process.chdir(currentWorkingDirectory); 140 | } 141 | 142 | core.debug(`Done`); 143 | // TODO(v4): Remove `version` 144 | core.setOutput('version', release); 145 | core.setOutput('release', release); 146 | } catch (error) { 147 | core.setFailed((error as Error).message); 148 | throw error; 149 | } 150 | } 151 | ); 152 | -------------------------------------------------------------------------------- /src/options.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import path from 'path'; 3 | import { getCLI } from './cli'; 4 | 5 | /** 6 | * Get the release version string from parameter or propose one. 7 | * @throws 8 | * @returns Promise 9 | */ 10 | export const getRelease = async (): Promise => { 11 | // TODO(v4): Remove `version` and `version_prefix`, they were deprecated in v3 12 | const releaseOption: string = core.getInput('release'); 13 | const versionOption: string = core.getInput('version'); 14 | const releasePrefixOption: string = core.getInput('release_prefix'); 15 | const versionPrefixOption: string = core.getInput('version_prefix'); 16 | 17 | let release = ''; 18 | if (releaseOption || versionOption) { 19 | // Prefer `release` over the deprecated `version` 20 | release = releaseOption ? releaseOption : versionOption; 21 | // If users pass `${{ github.ref }}, strip the unwanted `refs/tags` prefix 22 | release = release.replace(/^(refs\/tags\/)/, ''); 23 | } else { 24 | core.debug('Release version not provided, proposing one...'); 25 | release = await getCLI().proposeVersion(); 26 | } 27 | 28 | if (releasePrefixOption) { 29 | release = `${releasePrefixOption}${release}`; 30 | } else if (versionPrefixOption) { 31 | release = `${versionPrefixOption}${release}`; 32 | } 33 | 34 | return release; 35 | }; 36 | 37 | /** 38 | * Get `environment`, a required parameter. 39 | * @throws 40 | * @returns string 41 | */ 42 | export const getEnvironment = (): string => { 43 | return core.getInput('environment'); 44 | }; 45 | 46 | /** 47 | * Optionally get a UNIX timestamp of when the deployment started. 48 | * Input timestamp may also be ISO 8601. 49 | * 50 | * @throws 51 | * @returns number 52 | */ 53 | export const getStartedAt = (): number | null => { 54 | const startedAtOption: string = core.getInput('started_at'); 55 | if (!startedAtOption) { 56 | return null; 57 | } 58 | 59 | // In sentry-cli, we parse integer first. 60 | const isStartedAtAnInteger = /^-?[\d]+$/.test(startedAtOption); 61 | const startedAtTimestamp = parseInt(startedAtOption); 62 | const startedAt8601 = Math.floor(Date.parse(startedAtOption) / 1000); 63 | 64 | let outputTimestamp; 65 | if (isStartedAtAnInteger && !isNaN(startedAtTimestamp)) { 66 | outputTimestamp = startedAtTimestamp; 67 | } else if (!isNaN(startedAt8601)) { 68 | outputTimestamp = startedAt8601; 69 | } 70 | 71 | if (!outputTimestamp || outputTimestamp < 0) { 72 | throw new Error('started_at not in valid format. Unix timestamp or ISO 8601 date expected'); 73 | } 74 | 75 | return outputTimestamp; 76 | }; 77 | 78 | /** 79 | * Source maps are optional, but there may be several as a space-separated list. 80 | * @returns string[] 81 | */ 82 | export const getSourcemaps = (): string[] => { 83 | const sourcemapsOption: string = core.getInput('sourcemaps'); 84 | if (!sourcemapsOption) { 85 | return []; 86 | } 87 | 88 | return sourcemapsOption.split(' '); 89 | }; 90 | 91 | /** 92 | * Dist is optional, but should be a string when provided. 93 | * @returns string 94 | */ 95 | export const getDist = (): string | undefined => { 96 | const distOption: string = core.getInput('dist'); 97 | if (!distOption) { 98 | return undefined; 99 | } 100 | 101 | return distOption; 102 | }; 103 | 104 | /** 105 | * Fetch boolean option from input. Throws error if option value is not a boolean. 106 | * @param input string 107 | * @param defaultValue boolean 108 | * @returns boolean 109 | */ 110 | export const getBooleanOption = (input: string, defaultValue: boolean): boolean => { 111 | const option = core.getInput(input); 112 | if (!option) { 113 | return defaultValue; 114 | } 115 | 116 | const value = option.trim().toLowerCase(); 117 | switch (value) { 118 | case 'true': 119 | case '1': 120 | return true; 121 | 122 | case 'false': 123 | case '0': 124 | return false; 125 | } 126 | 127 | throw Error(`${input} is not a boolean`); 128 | }; 129 | 130 | export const getSetCommitsOption = (): 'auto' | 'skip' | 'manual' => { 131 | let setCommitOption = core.getInput('set_commits'); 132 | // default to auto 133 | if (!setCommitOption) { 134 | return 'auto'; 135 | } 136 | // convert to lower case 137 | setCommitOption = setCommitOption.toLowerCase(); 138 | switch (setCommitOption) { 139 | case 'auto': 140 | return 'auto'; 141 | case 'skip': 142 | return 'skip'; 143 | case 'manual': 144 | return 'manual'; 145 | default: 146 | throw Error('set_commits must be "auto", "skip" or "manual"'); 147 | } 148 | }; 149 | 150 | export const getSetCommitsManualOptions = (): { repo: string; commit: string; previousCommit: string } => { 151 | const repo = core.getInput('repo'); 152 | const commit = core.getInput('commit'); 153 | const previousCommit = core.getInput('previous_commit'); 154 | 155 | return { repo, commit, previousCommit }; 156 | }; 157 | /** 158 | * Check for required environment variables. 159 | */ 160 | export const checkEnvironmentVariables = (): void => { 161 | if (process.env['MOCK']) { 162 | // Set environment variables for mock runs if they aren't already 163 | for (const variable of ['SENTRY_AUTH_TOKEN', 'SENTRY_ORG', 'SENTRY_PROJECT']) { 164 | if (!(variable in process.env)) { 165 | process.env[variable] = variable; 166 | } 167 | } 168 | } 169 | 170 | if (!process.env['SENTRY_ORG']) { 171 | throw Error('Environment variable SENTRY_ORG is missing an organization slug'); 172 | } 173 | if (!process.env['SENTRY_AUTH_TOKEN']) { 174 | throw Error('Environment variable SENTRY_AUTH_TOKEN is missing an auth token'); 175 | } 176 | }; 177 | 178 | export const getProjects = (): string[] => { 179 | const projectsOption = core.getInput('projects') || ''; 180 | const projects = projectsOption 181 | .split(' ') 182 | .map(proj => proj.trim()) 183 | .filter(proj => !!proj); 184 | if (projects.length > 0) { 185 | return projects; 186 | } 187 | const project = process.env['SENTRY_PROJECT']; 188 | if (!project) { 189 | throw Error( 190 | 'Environment variable SENTRY_PROJECT is missing a project slug and no projects are specified with the "projects" option' 191 | ); 192 | } 193 | return [project]; 194 | }; 195 | 196 | export const getUrlPrefixOption = (): string => { 197 | return core.getInput('url_prefix'); 198 | }; 199 | 200 | export const getWorkingDirectory = (): string => { 201 | // The action runs inside `github.action_path` and as such 202 | // doesn't automatically have access to the user's git 203 | // We prefix all paths with `GITHUB_WORKSPACE` which is at the top of the repo. 204 | return path.join(process.env.GITHUB_WORKSPACE || '', core.getInput('working_directory')); 205 | }; 206 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | pull_request: 5 | paths-ignore: 6 | - '**.md' 7 | push: 8 | branches: 9 | - master 10 | - release/** 11 | paths-ignore: 12 | - '**.md' 13 | 14 | env: 15 | # Variables defined in the repository 16 | SENTRY_ORG: ${{ vars.SENTRY_ORG }} 17 | # For master, we have an environment variable that selects the action-release project 18 | # instead of action-release-prs 19 | # For other branches: https://sentry-ecosystem.sentry.io/releases/?project=4505075304693760 20 | # For master branch: https://sentry-ecosystem.sentry.io/releases/?project=6576594 21 | SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT }} 22 | 23 | jobs: 24 | prepare-docker: 25 | name: Prepare docker tag 26 | runs-on: ubuntu-latest 27 | permissions: 28 | contents: write 29 | outputs: 30 | docker_tag: ${{ steps.docker_tag.outputs.docker_tag }} 31 | steps: 32 | - name: Checkout repo 33 | uses: actions/checkout@v4 34 | 35 | - name: Get docker tag 36 | id: docker_tag 37 | run: | 38 | if [[ "${{ github.ref }}" == "refs/heads/master" ]]; then 39 | echo "docker_tag=master" >> $GITHUB_OUTPUT 40 | yarn set-docker-tag master 41 | else 42 | TAG=$(yq '... | select(has("uses") and .uses | test("docker://ghcr.io/getsentry/action-release-image:.*")) | .uses' action.yml | awk -F':' '{print $3}') 43 | echo "docker_tag=$TAG" >> $GITHUB_OUTPUT 44 | 45 | if [[ "${{ github.event_name }}" == "pull_request" ]]; then 46 | if [[ "$TAG" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then 47 | echo "Error: docker_tag $TAG matching format MAJOR.MINOR.PATCH is not allowed inside pull requests." 48 | echo "Please rename the docker tag in action.yml and try again." 49 | exit 1 50 | fi 51 | fi 52 | fi 53 | 54 | - name: Get auth token 55 | id: token 56 | uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0 57 | if: github.ref == 'refs/heads/master' 58 | with: 59 | app_id: ${{ vars.SENTRY_INTERNAL_APP_ID }} 60 | private_key: ${{ secrets.SENTRY_INTERNAL_APP_PRIVATE_KEY }} 61 | 62 | - name: Commit changes 63 | uses: getsentry/action-github-commit@v2.0.0 64 | if: github.ref == 'refs/heads/master' 65 | with: 66 | github-token: ${{ steps.token.outputs.token }} 67 | message: 'chore: Set docker tag for master [skip ci]' 68 | 69 | docker-build: 70 | name: Build & publish Docker images 71 | needs: prepare-docker 72 | runs-on: ubuntu-latest 73 | permissions: 74 | packages: write 75 | strategy: 76 | matrix: 77 | target: 78 | - name: builder 79 | image: action-release-builder-image 80 | - name: app 81 | image: action-release-image 82 | steps: 83 | - name: Checkout repo 84 | uses: actions/checkout@v4 85 | with: 86 | fetch-depth: 0 87 | 88 | - name: Set up QEMU 89 | uses: docker/setup-qemu-action@v3 90 | 91 | - name: Set up Docker Buildx 92 | uses: docker/setup-buildx-action@v3 93 | 94 | - name: Login to GitHub Container Registry 95 | uses: docker/login-action@v3 96 | with: 97 | registry: ghcr.io 98 | username: ${{ github.actor }} 99 | password: ${{ secrets.GITHUB_TOKEN }} 100 | 101 | # BUILDKIT_INLINE_CACHE creates the image in such a way that you can 102 | # then use --cache-from (think of a remote cache) 103 | # This feature is allowed thanks to using the buildx plugin 104 | # 105 | # There's a COPY command in the builder stage that can easily invalidate the cache 106 | # If you notice, please add more exceptions to .dockerignore since we loose the value 107 | # of using --cache-from on the app stage 108 | - name: Build and push 109 | uses: docker/build-push-action@v6 110 | with: 111 | platforms: linux/amd64,linux/arm64 112 | push: true 113 | tags: ghcr.io/${{ github.repository_owner }}/${{ matrix.target.image }}:${{ needs.prepare-docker.outputs.docker_tag }} 114 | cache-from: ghcr.io/${{ github.repository_owner }}/${{ matrix.target.image }}:master 115 | target: ${{ matrix.target.name }} 116 | build-args: BUILDKIT_INLINE_CACHE=1 117 | 118 | lint: 119 | runs-on: ubuntu-latest 120 | 121 | steps: 122 | - uses: actions/checkout@v4 123 | 124 | - name: Install 125 | run: yarn install 126 | 127 | - name: Check format 128 | run: yarn format-check 129 | 130 | - name: Lint 131 | run: yarn lint 132 | 133 | - name: Build 134 | run: yarn build 135 | 136 | ############# 137 | # E2E Tests 138 | ############# 139 | 140 | test-create-staging-release-per-push: 141 | needs: docker-build 142 | strategy: 143 | matrix: 144 | os: [ubuntu-latest, windows-latest, macos-latest] 145 | runs-on: ${{ matrix.os }} 146 | permissions: 147 | contents: read 148 | name: Test current action 149 | steps: 150 | - uses: actions/checkout@v4 151 | with: 152 | fetch-depth: 0 153 | 154 | - name: Create a staging release 155 | uses: ./ 156 | env: 157 | SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} 158 | SENTRY_LOG_LEVEL: debug 159 | with: 160 | ignore_missing: true 161 | 162 | test-runs-on-container: 163 | needs: docker-build 164 | runs-on: ubuntu-latest 165 | permissions: 166 | contents: read 167 | container: 168 | image: node:20.19.2 169 | 170 | steps: 171 | - uses: actions/checkout@v4 172 | with: 173 | fetch-depth: 0 174 | 175 | - name: Create a staging release 176 | uses: ./ 177 | env: 178 | SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} 179 | SENTRY_LOG_LEVEL: debug 180 | with: 181 | ignore_missing: true 182 | 183 | test-mock-release: 184 | needs: docker-build 185 | strategy: 186 | matrix: 187 | os: [ubuntu-latest, windows-latest, macos-latest] 188 | runs-on: ${{ matrix.os }} 189 | name: Mock a release 190 | permissions: 191 | contents: read 192 | steps: 193 | - uses: actions/checkout@v4 194 | with: 195 | fetch-depth: 0 196 | 197 | - name: Mock creating a Sentry release 198 | uses: ./ 199 | env: 200 | MOCK: true 201 | with: 202 | environment: production 203 | 204 | test-mock-release-working-directory: 205 | needs: docker-build 206 | strategy: 207 | matrix: 208 | os: [ubuntu-latest, windows-latest, macos-latest] 209 | runs-on: ${{ matrix.os }} 210 | name: Mock a release in a different working directory 211 | permissions: 212 | contents: read 213 | steps: 214 | - name: Checkout directory we'll be running from 215 | uses: actions/checkout@v4 216 | with: 217 | fetch-depth: 0 218 | path: main/ 219 | 220 | - name: Checkout directory we'll be testing 221 | uses: actions/checkout@v4 222 | with: 223 | fetch-depth: 0 224 | path: test/ 225 | 226 | - name: Mock creating a Sentry release in a different directory 227 | uses: ./main 228 | env: 229 | MOCK: true 230 | with: 231 | environment: production 232 | working_directory: ./test 233 | 234 | test-node-version-preserved: 235 | needs: docker-build 236 | strategy: 237 | matrix: 238 | os: [ubuntu-latest, windows-latest, macos-latest] 239 | node-version: ['20.x', '22.x'] 240 | runs-on: ${{ matrix.os }} 241 | name: Test Node version preserved on ${{ matrix.os }} with Node ${{ matrix.node-version }} 242 | permissions: 243 | contents: read 244 | steps: 245 | - uses: actions/checkout@v4 246 | with: 247 | fetch-depth: 0 248 | 249 | - name: Setup Node 250 | uses: actions/setup-node@v4 251 | with: 252 | node-version: ${{ matrix.node-version }} 253 | 254 | - name: Print Node Version (Before) 255 | id: node_before 256 | shell: bash 257 | run: | 258 | VERSION=$(node --version) 259 | echo "Node version before: $VERSION" 260 | echo "VERSION=$VERSION" >> $GITHUB_OUTPUT 261 | 262 | - name: Mock creating a Sentry release 263 | uses: ./ 264 | env: 265 | MOCK: true 266 | with: 267 | environment: production 268 | 269 | - name: Print Node Version (After) 270 | shell: bash 271 | run: | 272 | VERSION_AFTER=$(node --version) 273 | echo "Node version after: $VERSION_AFTER" 274 | echo "Expected: ${{ steps.node_before.outputs.VERSION }}" 275 | if [ "$VERSION_AFTER" != "${{ steps.node_before.outputs.VERSION }}" ]; then 276 | echo "ERROR: Node version changed from ${{ steps.node_before.outputs.VERSION }} to $VERSION_AFTER" 277 | exit 1 278 | fi 279 | 280 | echo "SUCCESS: Node version preserved" 281 | 282 | test-manual-commit-range: 283 | needs: docker-build 284 | 285 | strategy: 286 | matrix: 287 | os: [ubuntu-latest, windows-latest, macos-latest] 288 | runs-on: ${{ matrix.os }} 289 | name: Test manual commit range 290 | permissions: 291 | contents: read 292 | steps: 293 | - uses: actions/checkout@v4 294 | with: 295 | fetch-depth: 0 296 | 297 | - name: Create a release with manual commit range 298 | uses: ./ 299 | env: 300 | SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} 301 | SENTRY_LOG_LEVEL: debug 302 | MOCK: true 303 | with: 304 | environment: production 305 | set_commits: manual 306 | repo: getsentry/action-release 307 | commit: ${{ github.sha }} 308 | previous_commit: ${{ github.sha }} 309 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'Sentry Release' 2 | description: 'GitHub Action for creating a release on Sentry' 3 | author: 'Sentry' 4 | 5 | inputs: 6 | environment: 7 | description: |- 8 | Set the environment for this release. E.g. "production" or "staging". 9 | Omit to skip adding deploy to release. 10 | required: false 11 | sourcemaps: 12 | description: |- 13 | Space-separated list of paths to JavaScript source maps. 14 | Omit to skip uploading sourcemaps. 15 | required: false 16 | inject: 17 | description: |- 18 | Injects Debug IDs into source files and source maps to ensure proper 19 | un-minifcation of your stacktraces. 20 | Does nothing if "sourcemaps" was not set. 21 | default: true 22 | required: false 23 | dist: 24 | description: |- 25 | Unique identifier for the distribution, used to further segment your 26 | release. 27 | Usually your build number. 28 | required: false 29 | finalize: 30 | description: |- 31 | When false, omit marking the release as finalized and released. 32 | default: true 33 | ignore_missing: 34 | description: |- 35 | When the flag is set and the previous release commit was not found in 36 | the repository, will create a release with the default commits count 37 | instead of failing the command. 38 | default: false 39 | required: false 40 | ignore_empty: 41 | description: |- 42 | When the flag is set, command will not fail and just exit silently if no 43 | new commits for a given release have been found. 44 | default: false 45 | required: false 46 | started_at: 47 | description: |- 48 | Unix timestamp of the release start date. Omit for current time. 49 | required: false 50 | version: 51 | description: |- 52 | Identifier that uniquely identifies the release. 53 | Should match the "release" property in your Sentry SDK init call if one 54 | was set. 55 | Omit to auto-generate one. 56 | deprecationMessage: |- 57 | Deprecated: Use "release" instead. 58 | required: false 59 | release: 60 | description: |- 61 | Identifier that uniquely identifies the release. 62 | Should match the "release" property in your Sentry SDK init call if one 63 | was set. 64 | Omit to auto-generate one. 65 | required: false 66 | version_prefix: 67 | description: |- 68 | Value prepended to auto-generated version. 69 | deprecationMessage: |- 70 | Deprecated: Use "release_prefix" instead. 71 | required: false 72 | release_prefix: 73 | description: |- 74 | Value prepended to auto-generated release version. 75 | required: false 76 | set_commits: 77 | description: |- 78 | Specify whether to set commits for the release. 79 | When "manual", you need to provide the repository, commit, and previous commit. 80 | One of: "auto", "skip", "manual" 81 | required: false 82 | repo: 83 | description: |- 84 | The repository to set commits for in "repo-owner/repo-name" format. 85 | Only used when "set_commits" is "manual". 86 | required: false 87 | commit: 88 | description: |- 89 | The commit SHA of the current release you are creating. 90 | Only used when "set_commits" is "manual". 91 | required: false 92 | previous_commit: 93 | description: |- 94 | The commit SHA of the previous release. 95 | Required when "set_commits" is "manual". 96 | required: false 97 | projects: 98 | description: |- 99 | Space-separated list of projects. 100 | Defaults to the env variable "SENTRY_PROJECT" if not provided. 101 | required: false 102 | url_prefix: 103 | description: |- 104 | Adds a prefix to source map urls after stripping them. 105 | required: false 106 | strip_common_prefix: 107 | description: |- 108 | Will remove a common prefix from uploaded filenames. Useful for removing 109 | a path that is build-machine-specific. 110 | Note: Will not remove common prefixes across two or more directories 111 | provided to "sourcemap". E.g. Setting 112 | "sourcemap": "./dist/js ./dist/asset/js" will strip "./dist" for the first 113 | directory and "./dist/assets/js" for the 114 | default: false 115 | required: false 116 | working_directory: 117 | description: |- 118 | Directory to collect sentry release information from. 119 | Useful when collecting information from a non-standard checkout directory. 120 | required: false 121 | disable_telemetry: 122 | description: |- 123 | The action sends telemetry data and crash reports to Sentry. This helps 124 | us improve the action. 125 | You can turn this off by setting this flag. 126 | default: false 127 | required: false 128 | disable_safe_directory: 129 | description: |- 130 | The action needs access to the repo it runs in. For that we need to 131 | configure git to mark the repo directory a safe directory. 132 | You can turn this off by setting this flag. 133 | default: false 134 | required: false 135 | 136 | runs: 137 | using: 'composite' 138 | steps: 139 | # For actions running on a linux runner, we use a docker 140 | # approach as it's faster and encapsulates everything needed 141 | # to run the action. 142 | - name: Run docker image 143 | if: runner.os != 'macOS' && runner.os != 'Windows' 144 | env: 145 | # Composite actions don't pass the outer action's inputs 146 | # down into these steps, so we have to replicate all inputs to be accessible 147 | # via @actions/core here and in the `Run Release Action` step further down. 148 | INPUT_ENVIRONMENT: ${{ inputs.environment }} 149 | INPUT_INJECT: ${{ inputs.inject }} 150 | INPUT_SOURCEMAPS: ${{ inputs.sourcemaps }} 151 | INPUT_DIST: ${{ inputs.dist }} 152 | INPUT_FINALIZE: ${{ inputs.finalize }} 153 | INPUT_IGNORE_MISSING: ${{ inputs.ignore_missing }} 154 | INPUT_IGNORE_EMPTY: ${{ inputs.ignore_empty }} 155 | INPUT_STARTED_AT: ${{ inputs.started_at }} 156 | INPUT_VERSION: ${{ inputs.version }} 157 | INPUT_RELEASE: ${{ inputs.release }} 158 | INPUT_VERSION_PREFIX: ${{ inputs.version_prefix }} 159 | INPUT_RELEASE_PREFIX: ${{ inputs.release_prefix }} 160 | INPUT_SET_COMMITS: ${{ inputs.set_commits }} 161 | INPUT_REPO: ${{ inputs.repo }} 162 | INPUT_COMMIT: ${{ inputs.commit }} 163 | INPUT_PREVIOUS_COMMIT: ${{ inputs.previous_commit }} 164 | INPUT_PROJECTS: ${{ inputs.projects }} 165 | INPUT_URL_PREFIX: ${{ inputs.url_prefix }} 166 | INPUT_STRIP_COMMON_PREFIX: ${{ inputs.strip_common_prefix }} 167 | INPUT_WORKING_DIRECTORY: ${{ inputs.working_directory }} 168 | INPUT_DISABLE_TELEMETRY: ${{ inputs.disable_telemetry }} 169 | INPUT_DISABLE_SAFE_DIRECTORY: ${{ inputs.disable_safe_directory }} 170 | uses: docker://ghcr.io/getsentry/action-release-image:master 171 | 172 | # For actions running on macos or windows runners, we use a composite 173 | # action approach which allows us to install the arch specific sentry-cli 174 | # binary that's needed for the runner. 175 | # This is slower than the docker approach but runs on macos and windows. 176 | - name: Mark GitHub workspace a safe directory in git 177 | if: ${{ (runner.os == 'macOS' || runner.os == 'Windows') && inputs.disable_safe_directory != 'true' }} 178 | shell: bash 179 | run: | 180 | git config --global --add safe.directory "$GITHUB_WORKSPACE" 181 | 182 | # Save the current Node version before changing it 183 | - name: Save current Node version 184 | if: runner.os == 'macOS' || runner.os == 'Windows' 185 | id: node_version 186 | shell: bash 187 | run: | 188 | if command -v node &> /dev/null; then 189 | echo "NODE_VERSION=$(node --version | sed 's/v//')" >> $GITHUB_OUTPUT 190 | fi 191 | 192 | - name: Setup node 193 | if: runner.os == 'macOS' || runner.os == 'Windows' 194 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 195 | with: 196 | # setup-node doesn't allow absolute paths, so we can't 197 | # just use `github.action_path` to read this out from the `package.json` 198 | # any changes to the runtime need to be reflected here 199 | node-version: 20.19.2 200 | 201 | - name: Install Sentry CLI v2 202 | if: runner.os == 'macOS' || runner.os == 'Windows' 203 | shell: bash 204 | run: npm install --no-package-lock @sentry/cli@^2.4 205 | working-directory: ${{ github.action_path }} 206 | 207 | - name: Run Release Action 208 | if: runner.os == 'macOS' || runner.os == 'Windows' 209 | env: 210 | # Composite actions don't pass the outer action's inputs 211 | # down into these steps, so we have to replicate all inputs to be accessible 212 | # via @actions/core 213 | INPUT_ENVIRONMENT: ${{ inputs.environment }} 214 | INPUT_INJECT: ${{ inputs.inject }} 215 | INPUT_SOURCEMAPS: ${{ inputs.sourcemaps }} 216 | INPUT_DIST: ${{ inputs.dist }} 217 | INPUT_FINALIZE: ${{ inputs.finalize }} 218 | INPUT_IGNORE_MISSING: ${{ inputs.ignore_missing }} 219 | INPUT_IGNORE_EMPTY: ${{ inputs.ignore_empty }} 220 | INPUT_STARTED_AT: ${{ inputs.started_at }} 221 | INPUT_VERSION: ${{ inputs.version }} 222 | INPUT_RELEASE: ${{ inputs.release }} 223 | INPUT_VERSION_PREFIX: ${{ inputs.version_prefix }} 224 | INPUT_RELEASE_PREFIX: ${{ inputs.release_prefix }} 225 | INPUT_SET_COMMITS: ${{ inputs.set_commits }} 226 | INPUT_REPO: ${{ inputs.repo }} 227 | INPUT_COMMIT: ${{ inputs.commit }} 228 | INPUT_PREVIOUS_COMMIT: ${{ inputs.previous_commit }} 229 | INPUT_PROJECTS: ${{ inputs.projects }} 230 | INPUT_URL_PREFIX: ${{ inputs.url_prefix }} 231 | INPUT_STRIP_COMMON_PREFIX: ${{ inputs.strip_common_prefix }} 232 | INPUT_WORKING_DIRECTORY: ${{ inputs.working_directory }} 233 | INPUT_DISABLE_TELEMETRY: ${{ inputs.disable_telemetry }} 234 | INPUT_DISABLE_SAFE_DIRECTORY: ${{ inputs.disable_safe_directory }} 235 | shell: bash 236 | run: npm run start 237 | working-directory: ${{ github.action_path }} 238 | 239 | # Restore the original Node version 240 | - name: Restore original Node version 241 | if: (runner.os == 'macOS' || runner.os == 'Windows') && steps.node_version.outputs.NODE_VERSION != '' 242 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 243 | with: 244 | node-version: ${{ steps.node_version.outputs.NODE_VERSION }} 245 | 246 | branding: 247 | icon: 'triangle' 248 | color: 'purple' 249 | -------------------------------------------------------------------------------- /__tests__/main.test.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process'; 2 | import * as path from 'path'; 3 | import * as process from 'process'; 4 | import { 5 | getBooleanOption, 6 | getDist, 7 | getSourcemaps, 8 | getStartedAt, 9 | getRelease, 10 | getSetCommitsOption, 11 | getProjects, 12 | getUrlPrefixOption, 13 | getWorkingDirectory, 14 | getSetCommitsManualOptions, 15 | } from '../src/options'; 16 | 17 | describe('options', () => { 18 | beforeAll(() => { 19 | process.env['MOCK'] = 'true'; 20 | }); 21 | 22 | describe('getBooleanOption', () => { 23 | const option = 'finalize'; 24 | const defaultValue = true; 25 | const errorMessage = `${option} is not a boolean`; 26 | 27 | afterEach(() => { 28 | delete process.env['INPUT_FINALIZE']; 29 | }); 30 | 31 | test('should throw an error when option type is not a boolean', () => { 32 | process.env['INPUT_FINALIZE'] = 'error'; 33 | expect(() => getBooleanOption(option, defaultValue)).toThrow(errorMessage); 34 | }); 35 | 36 | test('should return defaultValue if option is omitted', () => { 37 | expect(getBooleanOption(option, defaultValue)).toBe(true); 38 | }); 39 | 40 | test('should return true when option is true or 1', () => { 41 | process.env['INPUT_FINALIZE'] = 'true'; 42 | expect(getBooleanOption(option, defaultValue)).toBe(true); 43 | process.env['INPUT_FINALIZE'] = '1'; 44 | expect(getBooleanOption(option, defaultValue)).toBe(true); 45 | }); 46 | 47 | test('should return false when option is false or 0', () => { 48 | process.env['INPUT_FINALIZE'] = 'false'; 49 | expect(getBooleanOption(option, defaultValue)).toBe(false); 50 | process.env['INPUT_FINALIZE'] = '0'; 51 | expect(getBooleanOption(option, defaultValue)).toBe(false); 52 | }); 53 | }); 54 | 55 | describe('getSourcemaps', () => { 56 | afterEach(() => { 57 | delete process.env['INPUT_SOURCEMAPS']; 58 | }); 59 | 60 | test('should return empty list when sourcemaps is omitted', async () => { 61 | expect(getSourcemaps()).toEqual([]); 62 | }); 63 | 64 | test('should return array when sourcemaps is false', () => { 65 | process.env['INPUT_SOURCEMAPS'] = './lib'; 66 | expect(getSourcemaps()).toEqual(['./lib']); 67 | }); 68 | }); 69 | 70 | describe('getDist', () => { 71 | afterEach(() => { 72 | delete process.env['INPUT_DIST']; 73 | }); 74 | 75 | test('should return undefined when dist is omitted', async () => { 76 | expect(getDist()).toBeUndefined(); 77 | }); 78 | 79 | test('should return a string when dist is provided', () => { 80 | process.env['INPUT_DIST'] = 'foo-dist'; 81 | expect(getDist()).toEqual('foo-dist'); 82 | }); 83 | }); 84 | 85 | describe('getStartedAt', () => { 86 | const errorMessage = 'started_at not in valid format. Unix timestamp or ISO 8601 date expected'; 87 | afterEach(() => { 88 | delete process.env['INPUT_STARTED_AT']; 89 | }); 90 | 91 | test('should throw an error when started_at is negative', async () => { 92 | process.env['INPUT_STARTED_AT'] = '-1'; 93 | expect(() => getStartedAt()).toThrow(errorMessage); 94 | }); 95 | 96 | test('should throw an error when started_at is invalid', async () => { 97 | process.env['INPUT_STARTED_AT'] = 'error'; 98 | expect(() => getStartedAt()).toThrow(errorMessage); 99 | }); 100 | 101 | test('should return null when started_at is omitted', async () => { 102 | expect(getStartedAt()).toBeNull(); 103 | }); 104 | 105 | test('should return an integer when started_at is an ISO8601 string.', async () => { 106 | process.env['INPUT_STARTED_AT'] = '2017-07-13T19:40:00-07:00'; 107 | expect(getStartedAt()).toEqual(1500000000); 108 | }); 109 | 110 | test('should return an integer when started_at is an truncated ISO8601 string.', async () => { 111 | process.env['INPUT_STARTED_AT'] = '2017-07-13'; 112 | expect(getStartedAt()).toEqual(1499904000); 113 | }); 114 | 115 | test('should return an integer when started_at is an integer.', async () => { 116 | process.env['INPUT_STARTED_AT'] = '1500000000'; 117 | expect(getStartedAt()).toEqual(1500000000); 118 | }); 119 | }); 120 | 121 | describe.each([ 122 | { release: 'INPUT_RELEASE', prefix: 'INPUT_RELEASE_PREFIX' }, 123 | { release: 'INPUT_VERSION', prefix: 'INPUT_VERSION_PREFIX' }, 124 | { release: 'INPUT_RELEASE', prefix: 'INPUT_VERSION_PREFIX' }, 125 | { release: 'INPUT_VERSION', prefix: 'INPUT_RELEASE_PREFIX' }, 126 | ])(`getRelease`, params => { 127 | const MOCK_VERSION = 'releases propose-version'; 128 | 129 | afterEach(() => { 130 | delete process.env['INPUT_RELEASE']; 131 | delete process.env['INPUT_VERSION']; 132 | delete process.env['INPUT_RELEASE_PREFIX']; 133 | delete process.env['INPUT_VERSION_PREFIX']; 134 | }); 135 | 136 | test(`should strip refs from ${params.release}`, async () => { 137 | process.env[params.release] = 'refs/tags/v1.0.0'; 138 | expect(await getRelease()).toBe('v1.0.0'); 139 | }); 140 | 141 | test(`should get release version from ${params.release}`, async () => { 142 | process.env[params.release] = 'v1.0.0'; 143 | expect(await getRelease()).toBe('v1.0.0'); 144 | }); 145 | 146 | test(`should propose-version when release version ${params.release} is omitted`, async () => { 147 | expect(await getRelease()).toBe(MOCK_VERSION); 148 | }); 149 | 150 | test(`should include ${params.prefix} prefix`, async () => { 151 | process.env[params.prefix] = 'prefix-'; 152 | expect(await getRelease()).toBe(`prefix-${MOCK_VERSION}`); 153 | }); 154 | 155 | test(`should include ${params.prefix} prefix with user provided release version ${params.release}`, async () => { 156 | process.env[params.release] = 'v1.0.0'; 157 | process.env[params.prefix] = 'prefix-'; 158 | expect(await getRelease()).toBe(`prefix-v1.0.0`); 159 | }); 160 | }); 161 | 162 | describe('getRelease', () => { 163 | afterEach(() => { 164 | delete process.env['INPUT_RELEASE']; 165 | delete process.env['INPUT_VERSION']; 166 | delete process.env['INPUT_RELEASE_PREFIX']; 167 | delete process.env['INPUT_VERSION_PREFIX']; 168 | }); 169 | 170 | test('should prefer INPUT_RELEASE over deprecated INPUT_VERSION', async () => { 171 | process.env['INPUT_VERSION'] = 'v0.0.1'; 172 | process.env['INPUT_RELEASE'] = 'v1.2.3'; 173 | expect(await getRelease()).toBe('v1.2.3'); 174 | }); 175 | 176 | test('should prefer INPUT_RELEASE_PREFIX over deprecated INPUT_VERSION_PREFIX', async () => { 177 | process.env['INPUT_VERSION_PREFIX'] = 'version-prefix-'; 178 | process.env['INPUT_RELEASE_PREFIX'] = 'release-prefix-'; 179 | process.env['INPUT_RELEASE'] = 'v1.2.3'; 180 | expect(await getRelease()).toBe('release-prefix-v1.2.3'); 181 | }); 182 | }); 183 | 184 | describe('getSetCommitsOption', () => { 185 | afterEach(() => { 186 | delete process.env['INPUT_SET_COMMITS']; 187 | }); 188 | 189 | it('no option', () => { 190 | expect(getSetCommitsOption()).toBe('auto'); 191 | }); 192 | it('auto', () => { 193 | process.env['INPUT_SET_COMMITS'] = 'auto'; 194 | expect(getSetCommitsOption()).toBe('auto'); 195 | }); 196 | it('skip', () => { 197 | process.env['INPUT_SET_COMMITS'] = 'skip'; 198 | expect(getSetCommitsOption()).toBe('skip'); 199 | }); 200 | it('manual', () => { 201 | process.env['INPUT_SET_COMMITS'] = 'manual'; 202 | expect(getSetCommitsOption()).toBe('manual'); 203 | }); 204 | it('bad option', () => { 205 | const errorMessage = 'set_commits must be "auto", "skip" or "manual"'; 206 | process.env['INPUT_SET_COMMITS'] = 'bad'; 207 | expect(() => getSetCommitsOption()).toThrow(errorMessage); 208 | }); 209 | }); 210 | 211 | describe('getSetCommitsManualOptions', () => { 212 | afterEach(() => { 213 | delete process.env['INPUT_SET_COMMITS']; 214 | delete process.env['INPUT_REPO']; 215 | delete process.env['INPUT_COMMIT']; 216 | delete process.env['INPUT_PREVIOUS_COMMIT']; 217 | }); 218 | it('manual', () => { 219 | process.env['INPUT_SET_COMMITS'] = 'manual'; 220 | process.env['INPUT_REPO'] = 'repo'; 221 | process.env['INPUT_COMMIT'] = 'commit'; 222 | process.env['INPUT_PREVIOUS_COMMIT'] = 'previous-commit'; 223 | expect(getSetCommitsManualOptions()).toEqual({ 224 | repo: 'repo', 225 | commit: 'commit', 226 | previousCommit: 'previous-commit', 227 | }); 228 | }); 229 | }); 230 | 231 | describe('getProjects', () => { 232 | afterEach(() => { 233 | delete process.env['SENTRY_PROJECT']; 234 | delete process.env['INPUT_PROJECTS']; 235 | }); 236 | it('read from env variable', () => { 237 | process.env['SENTRY_PROJECT'] = 'my-proj'; 238 | expect(getProjects()).toEqual(['my-proj']); 239 | }); 240 | it('read from option', () => { 241 | process.env['INPUT_PROJECTS'] = 'my-proj1 my-proj2'; 242 | expect(getProjects()).toEqual(['my-proj1', 'my-proj2']); 243 | }); 244 | it('option overwites env variable', () => { 245 | process.env['SENTRY_PROJECT'] = 'my-proj'; 246 | process.env['INPUT_PROJECTS'] = 'my-proj1 my-proj2'; 247 | expect(getProjects()).toEqual(['my-proj1', 'my-proj2']); 248 | }); 249 | it('throws error if no project', () => { 250 | expect(() => getProjects()).toThrowError( 251 | 'Environment variable SENTRY_PROJECT is missing a project slug and no projects are specified with the "projects" option' 252 | ); 253 | }); 254 | }); 255 | 256 | describe('getUrlPrefixOption', () => { 257 | afterEach(() => { 258 | delete process.env['URL_PREFIX']; 259 | }); 260 | it('get url prefix', () => { 261 | process.env['INPUT_URL_PREFIX'] = 'build'; 262 | expect(getUrlPrefixOption()).toEqual('build'); 263 | }); 264 | }); 265 | 266 | describe('getWorkingDirectory', () => { 267 | afterEach(() => { 268 | delete process.env['GITHUB_WORKSPACE']; 269 | delete process.env['INPUT_WORKING_DIRECTORY']; 270 | }); 271 | 272 | it('gets the working directory url and prefixes it with the `GITHUB_WORKSPACE`', () => { 273 | process.env['GITHUB_WORKSPACE'] = '/repo/root'; 274 | process.env['INPUT_WORKING_DIRECTORY'] = '/some/working/directory'; 275 | expect(getWorkingDirectory()).toEqual('/repo/root/some/working/directory'); 276 | }); 277 | 278 | it('should default to `GITHUB_WORKSPACE` even if no direcotry is passed', () => { 279 | process.env['GITHUB_WORKSPACE'] = '/repo/root'; 280 | expect(getWorkingDirectory()).toEqual('/repo/root'); 281 | }); 282 | }); 283 | }); 284 | 285 | // shows how the runner will run a javascript action with env / stdout protocol 286 | test('test runs', () => { 287 | const output = execSync(`node ${path.join(__dirname, '..', 'dist', 'index.js')}`, { 288 | env: { 289 | ...process.env, 290 | INPUT_ENVIRONMENT: 'production', 291 | MOCK: 'true', 292 | SENTRY_AUTH_TOKEN: 'test_token', 293 | SENTRY_ORG: 'test_org', 294 | SENTRY_PROJECT: 'test_project', 295 | }, 296 | }); 297 | 298 | console.log(output.toString()); 299 | }); 300 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 | 6 | Sentry 7 | 8 | 9 |

10 | 11 | # Sentry Release GitHub Action 12 | 13 | Automatically create a Sentry release in a workflow. 14 | 15 | A release is a version of your code that can be deployed to an environment. When you give Sentry information about your releases, you unlock a number of new features: 16 | 17 | - Determine the issues and regressions introduced in a new release 18 | - Predict which commit caused an issue and who is likely responsible 19 | - Resolve issues by including the issue number in your commit message 20 | - Receive email notifications when your code gets deployed 21 | 22 | Additionally, releases are used for applying [source maps](https://docs.sentry.io/platforms/javascript/sourcemaps/) to minified JavaScript to view original, untransformed source code. You can learn more about releases in the [releases documentation](https://docs.sentry.io/workflow/releases). 23 | 24 | ## What's new 25 | 26 | Version 3 is out with improved support for source maps. We highly recommend upgrading to `getsentry/action-release@v3`. 27 | 28 | Please refer to the [release page](https://github.com/getsentry/action-release/releases) for the latest release notes. 29 | 30 | ## Prerequisites 31 | 32 | See how to [set up the prerequisites](https://docs.sentry.io/product/releases/setup/release-automation/github-actions/#prerequisites) for the Action to securely communicate with Sentry. 33 | 34 | ## Usage 35 | 36 | Adding the following to your workflow will create a new Sentry release and tell Sentry that you are deploying to the `production` environment. 37 | 38 | > [!IMPORTANT] 39 | > Make sure you are using at least v3 of [actions/checkout](https://github.com/actions/checkout) with `fetch-depth: 0`, issues commonly occur with older versions. 40 | 41 | ```yaml 42 | - uses: actions/checkout@v4 43 | with: 44 | fetch-depth: 0 45 | 46 | - name: Create Sentry release 47 | uses: getsentry/action-release@v3 48 | env: 49 | SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} 50 | SENTRY_ORG: ${{ secrets.SENTRY_ORG }} 51 | SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} 52 | # SENTRY_URL: https://sentry.io/ 53 | with: 54 | environment: production 55 | ``` 56 | 57 | ### Inputs 58 | 59 | #### Environment Variables 60 | 61 | | name | description | default | 62 | | ------------------- | -------------------------------------------------------------------------------------------------------------------- | -------------------- | 63 | | `SENTRY_AUTH_TOKEN` | **[Required]** Authentication token for Sentry. See [installation](#create-a-sentry-internal-integration). | - | 64 | | `SENTRY_ORG` | **[Required]** The slug of the organization name in Sentry. | - | 65 | | `SENTRY_PROJECT` | The slug of the project name in Sentry. One of `SENTRY_PROJECT` or `projects` is required. | - | 66 | | `SENTRY_URL` | The URL used to connect to Sentry. (Only required for [Self-Hosted Sentry](https://develop.sentry.dev/self-hosted/)) | `https://sentry.io/` | 67 | 68 | #### Parameters 69 | 70 | | name | description | default | 71 | | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | 72 | | `environment` | Set the environment for this release. E.g. "production" or "staging". Omit to skip adding deploy to release. | - | 73 | | `sourcemaps` | Space-separated list of paths to JavaScript sourcemaps. Omit to skip uploading sourcemaps. | - | 74 | | `inject` | Injects Debug IDs into source files and source maps to ensure proper un-minifcation of your stacktraces. Does nothing if `sourcemaps` was not set. | `true` | 75 | | `finalize` | When false, omit marking the release as finalized and released. | `true` | 76 | | `ignore_missing` | When the flag is set and the previous release commit was not found in the repository, will create a release with the default commits count instead of failing the command. | `false` | 77 | | `ignore_empty` | When the flag is set, command will not fail and just exit silently if no new commits for a given release have been found. | `false` | 78 | | `dist` | Unique identifier for the distribution, used to further segment your release. Usually your build number. | - | 79 | | `started_at` | Unix timestamp of the release start date. Omit for current time. | - | 80 | | `release` | Identifier that uniquely identifies the releases. Should match the `release` property in your Sentry SDK init call if one was set. _Note: the `refs/tags/` prefix is automatically stripped when `version` is `github.ref`._ | ${{ github.sha }} | 81 | | `version` | Deprecated: Use `release` instead. | ${{ github.sha }} | 82 | | `release_prefix` | Value prepended to auto-generated version. For example "v". | - | 83 | | `version_prefix` | Deprecated: Use `release_prefix` instead. | - | 84 | | `set_commits` | Specify whether to set commits for the release. When "manual", you need to provide the repository, commit, and previous commit. One of "auto", "skip" or "manual". | "auto" | 85 | | `repo` | The repository to set commits for in "repo-owner/repo-name" format. Only used when `set_commits` is "manual". | - | 86 | | `commit` | The commit SHA of the current release you are creating. Only used when `set_commits` is "manual". | - | 87 | | `previous_commit` | The commit SHA of the previous release. Required when `set_commits` is "manual". | - | 88 | | `projects` | Space-separated list of paths of projects. When omitted, falls back to the environment variable `SENTRY_PROJECT` to determine the project. | - | 89 | | `url_prefix` | Adds a prefix to source map urls after stripping them. | - | 90 | | `strip_common_prefix` | Will remove a common prefix from uploaded filenames. Useful for removing a path that is build-machine-specific. | `false` | 91 | | `working_directory` | Directory to collect sentry release information from. Useful when collecting information from a non-standard checkout directory. | - | 92 | | `disable_telemetry` | The action sends telemetry data and crash reports to Sentry. This helps us improve the action. You can turn this off by setting this flag. | `false` | 93 | | `disable_safe_directory` | The action needs access to the repo it runs in. For that we need to configure git to mark the repo as a safe directory. You can turn this off by setting this flag. | `false` | 94 | 95 | ### Examples 96 | 97 | - Create a new Sentry release for the `production` environment, inject Debug IDs into JavaScript source files and source maps and upload them from the `./dist` directory. 98 | 99 | ```yaml 100 | - uses: getsentry/action-release@v3 101 | with: 102 | environment: 'production' 103 | sourcemaps: './dist' 104 | ``` 105 | 106 | - Create a new Sentry release for the `production` environment of your project at version `v1.0.1`. 107 | 108 | ```yaml 109 | - uses: getsentry/action-release@v3 110 | with: 111 | environment: 'production' 112 | release: 'v1.0.1' 113 | ``` 114 | 115 | - Create a new Sentry release for [Self-Hosted Sentry](https://develop.sentry.dev/self-hosted/) 116 | 117 | ```yaml 118 | - uses: getsentry/action-release@v3 119 | env: 120 | SENTRY_URL: https://sentry.example.com/ 121 | ``` 122 | 123 | - Manually specify the commit range for the release. 124 | 125 | ```yaml 126 | - uses: getsentry/action-release@v3 127 | with: 128 | set_commits: manual 129 | repo: your-org/your-repo 130 | commit: ${{ github.sha }} 131 | previous_commit: 132 | ``` 133 | 134 | ## Contributing 135 | 136 | See the [Contributing Guide](./CONTRIBUTING.md). 137 | 138 | ## License 139 | 140 | See the [License File](./LICENSE) 141 | 142 | ## Troubleshooting 143 | 144 | Suggestions and issues can be posted on the repository's 145 | [issues page](https://github.com/getsentry/action-release/issues). 146 | 147 | - Forgetting to include the required environment variables 148 | (`SENTRY_AUTH_TOKEN`, `SENTRY_ORG`, and `SENTRY_PROJECT`), yields an error that looks like: 149 | 150 | ```text 151 | Environment variable SENTRY_ORG is missing an organization slug 152 | ``` 153 | 154 | - Building and running this action locally on an unsupported environment yields an error that looks like: 155 | 156 | ```text 157 | Syntax error: end of file unexpected (expecting ")") 158 | ``` 159 | 160 | - When adding the action, make sure to first check out your repo with `actions/checkout@v4`. 161 | Otherwise, it could fail at the `propose-version` step with the message: 162 | 163 | ```text 164 | error: Could not automatically determine release name 165 | ``` 166 | 167 | - In `actions/checkout@v4` the default fetch depth is 1. If you're getting the error message: 168 | 169 | ```text 170 | error: Could not find the SHA of the previous release in the git history. Increase your git clone depth. 171 | ``` 172 | 173 | you can fetch all history for all branches and tags by setting the `fetch-depth` to zero like so: 174 | 175 | ```yaml 176 | - uses: actions/checkout@v4 177 | with: 178 | fetch-depth: 0 179 | ``` 180 | 181 | - Not finding the repository 182 | 183 | ```text 184 | Error: Command failed: /action-release/node_modules/@sentry/cli-linux-x64/bin/sentry-cli --header sentry-trace:ab7a03b5cd8ce324103b3ced985de08b-2bf7fecfb8a1e812-1 --header baggage:sentry-environment=production-sentry-github-action,sentry-release=1.10.5,sentry-public_key=,sentry-trace_id=ab7a03b5cd8ce324103b3ced985de08b,sentry-sample_rate=1,sentry-transaction=sentry-github-action-execution,sentry-sampled=true releases set-commits action-test --auto 185 | error: could not find repository at '.'; class=Repository (6); code=NotFound (-3) 186 | ``` 187 | 188 | Ensure you use `actions/checkout` before running the action 189 | 190 | ```yaml 191 | - uses: actions/checkout@v4 192 | with: 193 | fetch-depth: 0 194 | 195 | - uses: getsentry/action-release@v3 196 | with: 197 | environment: 'production' 198 | release: 'v1.0.1' 199 | ``` 200 | --------------------------------------------------------------------------------