Compare commits

..

4 Commits

Author SHA1 Message Date
softprops
5970a9ee86 latest ncc 2021-12-05 23:34:28 -05:00
softprops
4d12fe762c checkin other ncc generated files 2021-12-05 23:34:07 -05:00
softprops
f20f71e9fd fmt 2021-12-05 23:25:22 -05:00
softprops
040bca2f2d reintroduce octokit retry and throttling plugins 2021-12-05 23:19:25 -05:00
23 changed files with 7283 additions and 4830 deletions

View File

@ -1,33 +0,0 @@
version: 2
updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: weekly
groups:
npm:
patterns:
- "*"
ignore:
- dependency-name: node-fetch
versions:
- ">=3.0.0"
# ignore mime and @types/mime per https://github.com/softprops/action-gh-release/pull/475
- dependency-name: mime
versions:
- ">=4.0.0"
- dependency-name: "@types/mime"
versions:
- ">=4.0.0"
commit-message:
prefix: "chore(deps)"
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: weekly
groups:
github-actions:
patterns:
- "*"
commit-message:
prefix: "chore(deps)"

22
.github/release.yml vendored
View File

@ -1,22 +0,0 @@
changelog:
exclude:
labels:
- ignore-for-release
- github-actions
authors:
- octocat
- renovate[bot]
categories:
- title: Breaking Changes 🛠
labels:
- breaking-change
- title: Exciting New Features 🎉
labels:
- enhancement
- feature
- title: Bug fixes 🐛
labels:
- bug
- title: Other Changes 🔄
labels:
- "*"

View File

@ -1,20 +1,14 @@
name: main name: Main
on: on: [pull_request, push]
push:
pull_request:
jobs: jobs:
build: build:
runs-on: ubuntu-24.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 # https://github.com/actions/checkout
- name: Checkout
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4 uses: actions/checkout@v2
with:
node-version-file: ".tool-versions"
cache: "npm"
- name: Install - name: Install
run: npm ci run: npm ci
- name: Build - name: Build
@ -29,4 +23,4 @@ jobs:
# git diff --exit-code --stat -- . ':!node_modules' \ # git diff --exit-code --stat -- . ':!node_modules' \
# || (echo "##[error] found changed files after build. please 'npm run build && npm run fmt'" \ # || (echo "##[error] found changed files after build. please 'npm run build && npm run fmt'" \
# "and check in all changes" \ # "and check in all changes" \
# && exit 1) # && exit 1)

View File

@ -1 +0,0 @@
nodejs 20.15.1

View File

@ -1,116 +1,3 @@
## 2.2.1
## What's Changed
### Bug fixes 🐛
* fix: big file uploads by @xen0n in https://github.com/softprops/action-gh-release/pull/562
### Other Changes 🔄
* chore(deps): bump @types/node from 22.10.1 to 22.10.2 by @dependabot in https://github.com/softprops/action-gh-release/pull/559
* chore(deps): bump @types/node from 22.10.2 to 22.10.5 by @dependabot in https://github.com/softprops/action-gh-release/pull/569
* chore: update error and warning messages for not matching files in files field by @ytimocin in https://github.com/softprops/action-gh-release/pull/568
## 2.2.0
## What's Changed
### Exciting New Features 🎉
* feat: read the release assets asynchronously by @xen0n in https://github.com/softprops/action-gh-release/pull/552
### Bug fixes 🐛
* fix(docs): clarify the default for tag_name by @alexeagle in https://github.com/softprops/action-gh-release/pull/544
### Other Changes 🔄
* chore(deps): bump typescript from 5.6.3 to 5.7.2 by @dependabot in https://github.com/softprops/action-gh-release/pull/548
* chore(deps): bump @types/node from 22.9.0 to 22.9.4 by @dependabot in https://github.com/softprops/action-gh-release/pull/547
* chore(deps): bump cross-spawn from 7.0.3 to 7.0.6 by @dependabot in https://github.com/softprops/action-gh-release/pull/545
* chore(deps): bump @vercel/ncc from 0.38.2 to 0.38.3 by @dependabot in https://github.com/softprops/action-gh-release/pull/543
* chore(deps): bump prettier from 3.3.3 to 3.4.1 by @dependabot in https://github.com/softprops/action-gh-release/pull/550
* chore(deps): bump @types/node from 22.9.4 to 22.10.1 by @dependabot in https://github.com/softprops/action-gh-release/pull/551
* chore(deps): bump prettier from 3.4.1 to 3.4.2 by @dependabot in https://github.com/softprops/action-gh-release/pull/554
## 2.1.0
## What's Changed
### Exciting New Features 🎉
* feat: add support for release assets with multiple spaces within the name by @dukhine in https://github.com/softprops/action-gh-release/pull/518
* feat: preserve upload order by @richarddd in https://github.com/softprops/action-gh-release/pull/500
### Other Changes 🔄
* chore(deps): bump @types/node from 22.8.2 to 22.8.7 by @dependabot in https://github.com/softprops/action-gh-release/pull/539
## 2.0.9
- maintenance release with updated dependencies
## 2.0.8
### Other Changes 🔄
* chore(deps): bump prettier from 2.8.0 to 3.3.3 by @dependabot in https://github.com/softprops/action-gh-release/pull/480
* chore(deps): bump @types/node from 20.14.9 to 20.14.11 by @dependabot in https://github.com/softprops/action-gh-release/pull/483
* chore(deps): bump @octokit/plugin-throttling from 9.3.0 to 9.3.1 by @dependabot in https://github.com/softprops/action-gh-release/pull/484
* chore(deps): bump glob from 10.4.2 to 11.0.0 by @dependabot in https://github.com/softprops/action-gh-release/pull/477
* refactor: write jest config in ts by @chenrui333 in https://github.com/softprops/action-gh-release/pull/485
* chore(deps): bump @actions/github from 5.1.1 to 6.0.0 by @dependabot in https://github.com/softprops/action-gh-release/pull/470
## 2.0.7
### Bug fixes 🐛
* Fix missing update release body by @FirelightFlagboy in https://github.com/softprops/action-gh-release/pull/365
### Other Changes 🔄
* Bump @octokit/plugin-retry from 4.0.3 to 7.1.1 by @dependabot in https://github.com/softprops/action-gh-release/pull/443
* Bump typescript from 4.9.5 to 5.5.2 by @dependabot in https://github.com/softprops/action-gh-release/pull/467
* Bump @types/node from 20.14.6 to 20.14.8 by @dependabot in https://github.com/softprops/action-gh-release/pull/469
* Bump @types/node from 20.14.8 to 20.14.9 by @dependabot in https://github.com/softprops/action-gh-release/pull/473
* Bump typescript from 5.5.2 to 5.5.3 by @dependabot in https://github.com/softprops/action-gh-release/pull/472
* Bump ts-jest from 29.1.5 to 29.2.2 by @dependabot in https://github.com/softprops/action-gh-release/pull/479
* docs: document that existing releases are updated by @jvanbruegge in https://github.com/softprops/action-gh-release/pull/474
## 2.0.6
- maintenance release with updated dependencies
## 2.0.5
- Factor in file names with spaces when upserting files [#446](https://github.com/softprops/action-gh-release/pull/446) via [@MystiPanda](https://github.com/MystiPanda)
- Improvements to error handling [#449](https://github.com/softprops/action-gh-release/pull/449) via [@till](https://github.com/till)
## 2.0.4
- Minor follow up to [#417](https://github.com/softprops/action-gh-release/pull/417). [#425](https://github.com/softprops/action-gh-release/pull/425)
## 2.0.3
- Declare `make_latest` as an input field in `action.yml` [#419](https://github.com/softprops/action-gh-release/pull/419)
## 2.0.2
- Revisit approach to [#384](https://github.com/softprops/action-gh-release/pull/384) making unresolved pattern failures opt-in [#417](https://github.com/softprops/action-gh-release/pull/417)
## 2.0.1
- Add support for make_latest property [#304](https://github.com/softprops/action-gh-release/pull/304) via [@samueljseay](https://github.com/samueljseay)
- Fail run if files setting contains invalid patterns [#384](https://github.com/softprops/action-gh-release/pull/384) via [@rpdelaney](https://github.com/rpdelaney)
- Add support for proxy env variables (don't use node-fetch) [#386](https://github.com/softprops/action-gh-release/pull/386/) via [@timor-raiman](https://github.com/timor-raiman)
- Suppress confusing warning when input_files is empty [#389](https://github.com/softprops/action-gh-release/pull/389) via [@Drowze](https://github.com/Drowze)
## 2.0.0
- `2.0.0`!? this release corrects a disjunction between git tag versions used in the marketplace and versions list this file. Previous versions should have really been 1.\*. Going forward this should be better aligned.
- Upgrade action.yml declaration to node20 to address deprecations
## 0.1.15
- Upgrade to action.yml declaration to node16 to address deprecations
- Upgrade dependencies
- Add `asset` output as a JSON array containing information about the uploaded assets - Add `asset` output as a JSON array containing information about the uploaded assets
## 0.1.14 ## 0.1.14

View File

@ -1,4 +1,4 @@
Copyright (c) 2019-current Doug Tangren Copyright (c) 2019 Doug Tangren
Permission is hereby granted, free of charge, to any person obtaining Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the a copy of this software and associated documentation files (the

View File

@ -21,16 +21,6 @@
<br /> <br />
- [🤸 Usage](#-usage)
- [🚥 Limit releases to pushes to tags](#-limit-releases-to-pushes-to-tags)
- [⬆️ Uploading release assets](#-uploading-release-assets)
- [📝 External release notes](#-external-release-notes)
- [💅 Customizing](#-customizing)
- [inputs](#inputs)
- [outputs](#outputs)
- [environment variables](#environment-variables)
- [Permissions](#permissions)
## 🤸 Usage ## 🤸 Usage
### 🚥 Limit releases to pushes to tags ### 🚥 Limit releases to pushes to tags
@ -51,10 +41,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v2
- name: Release - name: Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v1
if: github.ref_type == 'tag' if: startsWith(github.ref, 'refs/tags/')
``` ```
You can also use push config tag filter You can also use push config tag filter
@ -72,9 +62,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v2
- name: Release - name: Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v1
``` ```
### ⬆️ Uploading release assets ### ⬆️ Uploading release assets
@ -85,7 +75,6 @@ GitHub release and all are optional.
A common case for GitHub releases is to upload your binary after its been validated and packaged. A common case for GitHub releases is to upload your binary after its been validated and packaged.
Use the `with.files` input to declare a newline-delimited list of glob expressions matching the files Use the `with.files` input to declare a newline-delimited list of glob expressions matching the files
you wish to upload to GitHub releases. If you'd like you can just list the files by name directly. you wish to upload to GitHub releases. If you'd like you can just list the files by name directly.
If a tag already has a GitHub release, the existing release will be updated with the release assets.
Below is an example of uploading a single asset named `Release.txt` Below is an example of uploading a single asset named `Release.txt`
@ -99,14 +88,14 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v2
- name: Build - name: Build
run: echo ${{ github.sha }} > Release.txt run: echo ${{ github.sha }} > Release.txt
- name: Test - name: Test
run: cat Release.txt run: cat Release.txt
- name: Release - name: Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v1
if: github.ref_type == 'tag' if: startsWith(github.ref, 'refs/tags/')
with: with:
files: Release.txt files: Release.txt
``` ```
@ -123,23 +112,21 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v2
- name: Build - name: Build
run: echo ${{ github.sha }} > Release.txt run: echo ${{ github.sha }} > Release.txt
- name: Test - name: Test
run: cat Release.txt run: cat Release.txt
- name: Release - name: Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v1
if: github.ref_type == 'tag' if: startsWith(github.ref, 'refs/tags/')
with: with:
files: | files: |
Release.txt Release.txt
LICENSE LICENSE
``` ```
> **⚠️ Note:** Notice the `|` in the yaml syntax above ☝️. That lets you effectively declare a multi-line yaml string. You can learn more about multi-line yaml syntax [here](https://yaml-multiline.info) > **⚠️ Note:** Notice the `|` in the yaml syntax above ☝️. That let's you effectively declare a multi-line yaml string. You can learn more about multi-line yaml syntax [here](https://yaml-multiline.info)
> **⚠️ Note for Windows:** Paths must use `/` as a separator, not `\`, as `\` is used to escape characters with special meaning in the pattern; for example, instead of specifying `D:\Foo.txt`, you must specify `D:/Foo.txt`. If you're using PowerShell, you can do this with `$Path = $Path -replace '\\','/'`
### 📝 External release notes ### 📝 External release notes
@ -157,18 +144,19 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v2
- name: Generate Changelog - name: Generate Changelog
run: echo "# Good things have arrived" > ${{ github.workspace }}-CHANGELOG.txt run: echo "# Good things have arrived" > ${{ github.workspace }}-CHANGELOG.txt
- name: Release - name: Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v1
if: github.ref_type == 'tag' if: startsWith(github.ref, 'refs/tags/')
with: with:
body_path: ${{ github.workspace }}-CHANGELOG.txt body_path: ${{ github.workspace }}-CHANGELOG.txt
repository: my_gh_org/my_gh_repo
# note you'll typically need to create a personal access token # note you'll typically need to create a personal access token
# with permissions to create releases in the other repo # with permissions to create releases in the other repo
token: ${{ secrets.CUSTOM_GITHUB_TOKEN }} token: ${{ secrets.CUSTOM_GITHUB_TOKEN }}
env:
GITHUB_REPOSITORY: my_gh_org/my_gh_repo
``` ```
### 💅 Customizing ### 💅 Customizing
@ -183,18 +171,15 @@ The following are optional as `step.with` keys
| `body_path` | String | Path to load text communicating notable changes in this release | | `body_path` | String | Path to load text communicating notable changes in this release |
| `draft` | Boolean | Indicator of whether or not this release is a draft | | `draft` | Boolean | Indicator of whether or not this release is a draft |
| `prerelease` | Boolean | Indicator of whether or not is a prerelease | | `prerelease` | Boolean | Indicator of whether or not is a prerelease |
| `preserve_order` | Boolean | Indicator of whether order of files should be preserved when uploading assets |
| `files` | String | Newline-delimited globs of paths to assets to upload for release | | `files` | String | Newline-delimited globs of paths to assets to upload for release |
| `name` | String | Name of the release. defaults to tag name | | `name` | String | Name of the release. defaults to tag name |
| `tag_name` | String | Name of a tag. defaults to `github.ref_name` | | `tag_name` | String | Name of a tag. defaults to `github.ref` |
| `fail_on_unmatched_files` | Boolean | Indicator of whether to fail if any of the `files` globs match nothing | | `fail_on_unmatched_files` | Boolean | Indicator of whether to fail if any of the `files` globs match nothing |
| `repository` | String | Name of a target repository in `<owner>/<repo>` format. Defaults to GITHUB_REPOSITORY env variable | | `repository` | String | Name of a target repository in `<owner>/<repo>` format. Defaults to GITHUB_REPOSITORY env variable |
| `target_commitish` | String | Commitish value that determines where the Git tag is created from. Can be any branch or commit SHA. Defaults to repository default branch. | | `target_commitish` | String | Commitish value that determines where the Git tag is created from. Can be any branch or commit SHA. |
| `token` | String | Secret GitHub Personal Access Token. Defaults to `${{ github.token }}` | | `token` | String | Secret GitHub Personal Access Token. Defaults to `${{ github.token }}` |
| `discussion_category_name` | String | If specified, a discussion of the specified category is created and linked to the release. The value must be a category that already exists in the repository. For more information, see ["Managing categories for discussions in your repository."](https://docs.github.com/en/discussions/managing-discussions-for-your-community/managing-categories-for-discussions-in-your-repository) | | `discussion_category_name` | String | If specified, a discussion of the specified category is created and linked to the release. The value must be a category that already exists in the repository. For more information, see ["Managing categories for discussions in your repository."](https://docs.github.com/en/discussions/managing-discussions-for-your-community/managing-categories-for-discussions-in-your-repository) |
| `generate_release_notes` | Boolean | Whether to automatically generate the name and body for this release. If name is specified, the specified name will be used; otherwise, a name will be automatically generated. If body is specified, the body will be pre-pended to the automatically generated notes. See the [GitHub docs for this feature](https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes) for more information | | `generate_release_notes` | Boolean | Whether to automatically generate the name and body for this release. If name is specified, the specified name will be used; otherwise, a name will be automatically generated. If body is specified, the body will be pre-pended to the automatically generated notes. See the [GitHub docs for this feature](https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes) for more information |
| `append_body` | Boolean | Append to existing body instead of overwriting it |
| `make_latest` | String | Specifies whether this release should be set as the latest release for the repository. Drafts and prereleases cannot be set as latest. Can be `true`, `false`, or `legacy`. Uses GitHub api defaults if not provided |
💡 When providing a `body` and `body_path` at the same time, `body_path` will be 💡 When providing a `body` and `body_path` at the same time, `body_path` will be
attempted first, then falling back on `body` if the path can not be read from. attempted first, then falling back on `body` if the path can not be read from.
@ -207,12 +192,12 @@ release will retain its original info.
The following outputs can be accessed via `${{ steps.<step-id>.outputs }}` from this action The following outputs can be accessed via `${{ steps.<step-id>.outputs }}` from this action
| Name | Type | Description | | Name | Type | Description |
| ------------ | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ------------ | ------ | --------------------------------------- |
| `url` | String | Github.com URL for the release | | `url` | String | Github.com URL for the release |
| `id` | String | Release ID | | `id` | String | Release ID |
| `upload_url` | String | URL for uploading assets to the release | | `upload_url` | String | URL for uploading assets to the release |
| `assets` | String | JSON array containing information about each uploaded asset, in the format given [here](https://docs.github.com/en/rest/releases/assets#get-a-release-asset) (minus the `uploader` field) | | `assets` | String | JSON array containing information about each uploaded asset, in the format given [here](https://docs.github.com/en/rest/reference/repos#upload-a-release-asset--code-samples) (minus the `uploader` field) |
As an example, you can use `${{ fromJSON(steps.<step-id>.outputs.assets)[0].browser_download_url }}` to get the download URL of the first asset. As an example, you can use `${{ fromJSON(steps.<step-id>.outputs.assets)[0].browser_download_url }}` to get the download URL of the first asset.
@ -236,17 +221,6 @@ permissions:
contents: write contents: write
``` ```
When used with `discussion_category_name`, additional permission is needed:
```yaml
permissions:
contents: write
discussions: write
```
[GitHub token permissions](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token) can be set for an individual job, workflow, or for Actions as a whole. [GitHub token permissions](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token) can be set for an individual job, workflow, or for Actions as a whole.
Note that if you intend to run workflows on the release event (`on: { release: { types: [published] } }`), you need to use
a personal access token for this action, as the [default `secrets.GITHUB_TOKEN` does not trigger another workflow](https://github.com/actions/create-release/issues/71).
Doug Tangren (softprops) 2019 Doug Tangren (softprops) 2019

View File

@ -1,5 +1,6 @@
//import * as assert from "assert";
//const assert = require('assert');
import * as assert from "assert"; import * as assert from "assert";
import { text } from "stream/consumers";
import { mimeOrDefault, asset } from "../src/github"; import { mimeOrDefault, asset } from "../src/github";
describe("github", () => { describe("github", () => {
@ -14,10 +15,11 @@ describe("github", () => {
describe("asset", () => { describe("asset", () => {
it("derives asset info from a path", async () => { it("derives asset info from a path", async () => {
const { name, mime, size } = asset("tests/data/foo/bar.txt"); const { name, mime, size, data } = asset("tests/data/foo/bar.txt");
assert.equal(name, "bar.txt"); assert.equal(name, "bar.txt");
assert.equal(mime, "text/plain"); assert.equal(mime, "text/plain");
assert.equal(size, 10); assert.equal(size, 10);
assert.equal(data.toString(), "release me");
}); });
}); });
}); });

View File

@ -5,8 +5,7 @@ import {
parseConfig, parseConfig,
parseInputFiles, parseInputFiles,
unmatchedPatterns, unmatchedPatterns,
uploadUrl, uploadUrl
alignAssetName,
} from "../src/util"; } from "../src/util";
import * as assert from "assert"; import * as assert from "assert";
@ -15,9 +14,9 @@ describe("util", () => {
it("strips template", () => { it("strips template", () => {
assert.equal( assert.equal(
uploadUrl( uploadUrl(
"https://uploads.github.com/repos/octocat/Hello-World/releases/1/assets{?name,label}", "https://uploads.github.com/repos/octocat/Hello-World/releases/1/assets{?name,label}"
), ),
"https://uploads.github.com/repos/octocat/Hello-World/releases/1/assets", "https://uploads.github.com/repos/octocat/Hello-World/releases/1/assets"
); );
}); });
}); });
@ -31,7 +30,7 @@ describe("util", () => {
it("parses newline and comma-delimited (and then some)", () => { it("parses newline and comma-delimited (and then some)", () => {
assert.deepStrictEqual( assert.deepStrictEqual(
parseInputFiles("foo,bar\nbaz,boom,\n\ndoom,loom "), parseInputFiles("foo,bar\nbaz,boom,\n\ndoom,loom "),
["foo", "bar", "baz", "boom", "doom", "loom"], ["foo", "bar", "baz", "boom", "doom", "loom"]
); );
}); });
}); });
@ -47,15 +46,13 @@ describe("util", () => {
input_body_path: undefined, input_body_path: undefined,
input_draft: false, input_draft: false,
input_prerelease: false, input_prerelease: false,
input_preserve_order: undefined,
input_files: [], input_files: [],
input_name: undefined, input_name: undefined,
input_tag_name: undefined, input_tag_name: undefined,
input_target_commitish: undefined, input_target_commitish: undefined,
input_discussion_category_name: undefined, input_discussion_category_name: undefined,
input_generate_release_notes: false, input_generate_release_notes: false
input_make_latest: undefined, })
}),
); );
}); });
it("uses input body path", () => { it("uses input body path", () => {
@ -69,15 +66,13 @@ describe("util", () => {
input_body_path: "__tests__/release.txt", input_body_path: "__tests__/release.txt",
input_draft: false, input_draft: false,
input_prerelease: false, input_prerelease: false,
input_preserve_order: undefined,
input_files: [], input_files: [],
input_name: undefined, input_name: undefined,
input_tag_name: undefined, input_tag_name: undefined,
input_target_commitish: undefined, input_target_commitish: undefined,
input_discussion_category_name: undefined, input_discussion_category_name: undefined,
input_generate_release_notes: false, input_generate_release_notes: false
input_make_latest: undefined, })
}),
); );
}); });
it("defaults to body path when both body and body path are provided", () => { it("defaults to body path when both body and body path are provided", () => {
@ -91,15 +86,13 @@ describe("util", () => {
input_body_path: "__tests__/release.txt", input_body_path: "__tests__/release.txt",
input_draft: false, input_draft: false,
input_prerelease: false, input_prerelease: false,
input_preserve_order: undefined,
input_files: [], input_files: [],
input_name: undefined, input_name: undefined,
input_tag_name: undefined, input_tag_name: undefined,
input_target_commitish: undefined, input_target_commitish: undefined,
input_discussion_category_name: undefined, input_discussion_category_name: undefined,
input_generate_release_notes: false, input_generate_release_notes: false
input_make_latest: undefined, })
}),
); );
}); });
}); });
@ -114,107 +107,95 @@ describe("util", () => {
// so we cover that in a test case here to ensure undefined values are actually // so we cover that in a test case here to ensure undefined values are actually
// resolved as undefined and not empty strings // resolved as undefined and not empty strings
INPUT_TARGET_COMMITISH: "", INPUT_TARGET_COMMITISH: "",
INPUT_DISCUSSION_CATEGORY_NAME: "", INPUT_DISCUSSION_CATEGORY_NAME: ""
}), }),
{ {
github_ref: "", github_ref: "",
github_repository: "", github_repository: "",
github_token: "", github_token: "",
input_append_body: false,
input_body: undefined, input_body: undefined,
input_body_path: undefined, input_body_path: undefined,
input_draft: undefined, input_draft: undefined,
input_prerelease: undefined, input_prerelease: undefined,
input_preserve_order: undefined,
input_files: [], input_files: [],
input_name: undefined, input_name: undefined,
input_tag_name: undefined, input_tag_name: undefined,
input_fail_on_unmatched_files: false, input_fail_on_unmatched_files: false,
input_target_commitish: undefined, input_target_commitish: undefined,
input_discussion_category_name: undefined, input_discussion_category_name: undefined,
input_generate_release_notes: false, input_generate_release_notes: false
input_make_latest: undefined, }
},
); );
}); });
it("parses basic config with commitish", () => { it("parses basic config with commitish", () => {
assert.deepStrictEqual( assert.deepStrictEqual(
parseConfig({ parseConfig({
INPUT_TARGET_COMMITISH: "affa18ef97bc9db20076945705aba8c516139abd", INPUT_TARGET_COMMITISH: "affa18ef97bc9db20076945705aba8c516139abd"
}), }),
{ {
github_ref: "", github_ref: "",
github_repository: "", github_repository: "",
github_token: "", github_token: "",
input_append_body: false,
input_body: undefined, input_body: undefined,
input_body_path: undefined, input_body_path: undefined,
input_draft: undefined, input_draft: undefined,
input_prerelease: undefined, input_prerelease: undefined,
input_files: [], input_files: [],
input_preserve_order: undefined,
input_name: undefined, input_name: undefined,
input_tag_name: undefined, input_tag_name: undefined,
input_fail_on_unmatched_files: false, input_fail_on_unmatched_files: false,
input_target_commitish: "affa18ef97bc9db20076945705aba8c516139abd", input_target_commitish: "affa18ef97bc9db20076945705aba8c516139abd",
input_discussion_category_name: undefined, input_discussion_category_name: undefined,
input_generate_release_notes: false, input_generate_release_notes: false
input_make_latest: undefined, }
},
); );
}); });
it("supports discussion category names", () => { it("supports discussion category names", () => {
assert.deepStrictEqual( assert.deepStrictEqual(
parseConfig({ parseConfig({
INPUT_DISCUSSION_CATEGORY_NAME: "releases", INPUT_DISCUSSION_CATEGORY_NAME: "releases"
}), }),
{ {
github_ref: "", github_ref: "",
github_repository: "", github_repository: "",
github_token: "", github_token: "",
input_append_body: false,
input_body: undefined, input_body: undefined,
input_body_path: undefined, input_body_path: undefined,
input_draft: undefined, input_draft: undefined,
input_prerelease: undefined, input_prerelease: undefined,
input_files: [], input_files: [],
input_preserve_order: undefined,
input_name: undefined, input_name: undefined,
input_tag_name: undefined, input_tag_name: undefined,
input_fail_on_unmatched_files: false, input_fail_on_unmatched_files: false,
input_target_commitish: undefined, input_target_commitish: undefined,
input_discussion_category_name: "releases", input_discussion_category_name: "releases",
input_generate_release_notes: false, input_generate_release_notes: false
input_make_latest: undefined, }
},
); );
}); });
it("supports generating release notes", () => { it("supports generating release notes", () => {
assert.deepStrictEqual( assert.deepStrictEqual(
parseConfig({ parseConfig({
INPUT_GENERATE_RELEASE_NOTES: "true", INPUT_GENERATE_RELEASE_NOTES: "true"
}), }),
{ {
github_ref: "", github_ref: "",
github_repository: "", github_repository: "",
github_token: "", github_token: "",
input_append_body: false,
input_body: undefined, input_body: undefined,
input_body_path: undefined, input_body_path: undefined,
input_draft: undefined, input_draft: undefined,
input_prerelease: undefined, input_prerelease: undefined,
input_preserve_order: undefined,
input_files: [], input_files: [],
input_name: undefined, input_name: undefined,
input_tag_name: undefined, input_tag_name: undefined,
input_fail_on_unmatched_files: false, input_fail_on_unmatched_files: false,
input_target_commitish: undefined, input_target_commitish: undefined,
input_discussion_category_name: undefined, input_discussion_category_name: undefined,
input_generate_release_notes: true, input_generate_release_notes: true
input_make_latest: undefined, }
},
); );
}); });
@ -223,29 +204,25 @@ describe("util", () => {
parseConfig({ parseConfig({
INPUT_DRAFT: "false", INPUT_DRAFT: "false",
INPUT_PRERELEASE: "true", INPUT_PRERELEASE: "true",
INPUT_PRESERVE_ORDER: "true",
GITHUB_TOKEN: "env-token", GITHUB_TOKEN: "env-token",
INPUT_TOKEN: "input-token", INPUT_TOKEN: "input-token"
}), }),
{ {
github_ref: "", github_ref: "",
github_repository: "", github_repository: "",
github_token: "env-token", github_token: "env-token",
input_append_body: false,
input_body: undefined, input_body: undefined,
input_body_path: undefined, input_body_path: undefined,
input_draft: false, input_draft: false,
input_prerelease: true, input_prerelease: true,
input_preserve_order: true,
input_files: [], input_files: [],
input_name: undefined, input_name: undefined,
input_tag_name: undefined, input_tag_name: undefined,
input_fail_on_unmatched_files: false, input_fail_on_unmatched_files: false,
input_target_commitish: undefined, input_target_commitish: undefined,
input_discussion_category_name: undefined, input_discussion_category_name: undefined,
input_generate_release_notes: false, input_generate_release_notes: false
input_make_latest: undefined, }
},
); );
}); });
it("uses input token as the source of GITHUB_TOKEN by default", () => { it("uses input token as the source of GITHUB_TOKEN by default", () => {
@ -253,106 +230,48 @@ describe("util", () => {
parseConfig({ parseConfig({
INPUT_DRAFT: "false", INPUT_DRAFT: "false",
INPUT_PRERELEASE: "true", INPUT_PRERELEASE: "true",
INPUT_TOKEN: "input-token", INPUT_TOKEN: "input-token"
}), }),
{ {
github_ref: "", github_ref: "",
github_repository: "", github_repository: "",
github_token: "input-token", github_token: "input-token",
input_append_body: false,
input_body: undefined, input_body: undefined,
input_body_path: undefined, input_body_path: undefined,
input_draft: false, input_draft: false,
input_prerelease: true, input_prerelease: true,
input_preserve_order: undefined,
input_files: [], input_files: [],
input_name: undefined, input_name: undefined,
input_tag_name: undefined, input_tag_name: undefined,
input_fail_on_unmatched_files: false, input_fail_on_unmatched_files: false,
input_target_commitish: undefined, input_target_commitish: undefined,
input_discussion_category_name: undefined, input_discussion_category_name: undefined,
input_generate_release_notes: false, input_generate_release_notes: false
input_make_latest: undefined, }
},
); );
}); });
it("parses basic config with draft and prerelease", () => { it("parses basic config with draft and prerelease", () => {
assert.deepStrictEqual( assert.deepStrictEqual(
parseConfig({ parseConfig({
INPUT_DRAFT: "false", INPUT_DRAFT: "false",
INPUT_PRERELEASE: "true", INPUT_PRERELEASE: "true"
}), }),
{ {
github_ref: "", github_ref: "",
github_repository: "", github_repository: "",
github_token: "", github_token: "",
input_append_body: false,
input_body: undefined, input_body: undefined,
input_body_path: undefined, input_body_path: undefined,
input_draft: false, input_draft: false,
input_prerelease: true, input_prerelease: true,
input_preserve_order: undefined,
input_files: [], input_files: [],
input_name: undefined, input_name: undefined,
input_tag_name: undefined, input_tag_name: undefined,
input_fail_on_unmatched_files: false, input_fail_on_unmatched_files: false,
input_target_commitish: undefined, input_target_commitish: undefined,
input_discussion_category_name: undefined, input_discussion_category_name: undefined,
input_generate_release_notes: false, input_generate_release_notes: false
input_make_latest: undefined, }
},
);
});
it("parses basic config where make_latest is passed", () => {
assert.deepStrictEqual(
parseConfig({
INPUT_MAKE_LATEST: "false",
}),
{
github_ref: "",
github_repository: "",
github_token: "",
input_append_body: false,
input_body: undefined,
input_body_path: undefined,
input_draft: undefined,
input_prerelease: undefined,
input_preserve_order: undefined,
input_files: [],
input_name: undefined,
input_tag_name: undefined,
input_fail_on_unmatched_files: false,
input_target_commitish: undefined,
input_discussion_category_name: undefined,
input_generate_release_notes: false,
input_make_latest: "false",
},
);
});
it("parses basic config with append_body", () => {
assert.deepStrictEqual(
parseConfig({
INPUT_APPEND_BODY: "true",
}),
{
github_ref: "",
github_repository: "",
github_token: "",
input_append_body: true,
input_body: undefined,
input_body_path: undefined,
input_draft: undefined,
input_prerelease: undefined,
input_preserve_order: undefined,
input_files: [],
input_name: undefined,
input_tag_name: undefined,
input_fail_on_unmatched_files: false,
input_target_commitish: undefined,
input_discussion_category_name: undefined,
input_generate_release_notes: false,
input_make_latest: undefined,
},
); );
}); });
}); });
@ -369,7 +288,7 @@ describe("util", () => {
it("resolves files given a set of paths", async () => { it("resolves files given a set of paths", async () => {
assert.deepStrictEqual( assert.deepStrictEqual(
paths(["tests/data/**/*", "tests/data/does/not/exist/*"]), paths(["tests/data/**/*", "tests/data/does/not/exist/*"]),
["tests/data/foo/bar.txt"], ["tests/data/foo/bar.txt"]
); );
}); });
}); });
@ -378,24 +297,8 @@ describe("util", () => {
it("returns the patterns that don't match any files", async () => { it("returns the patterns that don't match any files", async () => {
assert.deepStrictEqual( assert.deepStrictEqual(
unmatchedPatterns(["tests/data/**/*", "tests/data/does/not/exist/*"]), unmatchedPatterns(["tests/data/**/*", "tests/data/does/not/exist/*"]),
["tests/data/does/not/exist/*"], ["tests/data/does/not/exist/*"]
); );
}); });
}); });
describe("replaceSpacesWithDots", () => {
it("replaces all spaces with dots", () => {
expect(alignAssetName("John Doe.bla")).toBe("John.Doe.bla");
});
it("handles names with multiple spaces", () => {
expect(alignAssetName("John William Doe.bla")).toBe(
"John.William.Doe.bla",
);
});
it("returns the same string if there are no spaces", () => {
expect(alignAssetName("JohnDoe")).toBe("JohnDoe");
});
});
}); });

View File

@ -13,7 +13,7 @@ inputs:
description: "Gives the release a custom name. Defaults to tag name" description: "Gives the release a custom name. Defaults to tag name"
required: false required: false
tag_name: tag_name:
description: "Gives a tag name. Defaults to github.ref_name" description: "Gives a tag name. Defaults to github.GITHUB_REF"
required: false required: false
draft: draft:
description: "Creates a draft release. Defaults to false" description: "Creates a draft release. Defaults to false"
@ -21,9 +21,6 @@ inputs:
prerelease: prerelease:
description: "Identify the release as a prerelease. Defaults to false" description: "Identify the release as a prerelease. Defaults to false"
required: false required: false
preserve_order:
description: "Preserver the order of the artifacts when uploading"
required: false
files: files:
description: "Newline-delimited list of path globs for asset files to upload" description: "Newline-delimited list of path globs for asset files to upload"
required: false required: false
@ -46,14 +43,8 @@ inputs:
generate_release_notes: generate_release_notes:
description: "Whether to automatically generate the name and body for this release. If name is specified, the specified name will be used; otherwise, a name will be automatically generated. If body is specified, the body will be pre-pended to the automatically generated notes." description: "Whether to automatically generate the name and body for this release. If name is specified, the specified name will be used; otherwise, a name will be automatically generated. If body is specified, the body will be pre-pended to the automatically generated notes."
required: false required: false
append_body:
description: "Append to existing body instead of overwriting it. Default is false."
required: false
make_latest:
description: "Specifies whether this release should be set as the latest release for the repository. Drafts and prereleases cannot be set as latest. Can be `true`, `false`, or `legacy`. Uses GitHub api default if not provided"
required: false
env: env:
GITHUB_TOKEN: "As provided by Github Actions" "GITHUB_TOKEN": "As provided by Github Actions"
outputs: outputs:
url: url:
description: "URL to the Release HTML Page" description: "URL to the Release HTML Page"
@ -64,7 +55,7 @@ outputs:
assets: assets:
description: "JSON array containing information about each uploaded asset, in the format given [here](https://docs.github.com/en/rest/reference/repos#upload-a-release-asset--code-samples) (minus the `uploader` field)" description: "JSON array containing information about each uploaded asset, in the format given [here](https://docs.github.com/en/rest/reference/repos#upload-a-release-asset--code-samples) (minus the `uploader` field)"
runs: runs:
using: "node20" using: "node12"
main: "dist/index.js" main: "dist/index.js"
branding: branding:
color: "green" color: "green"

1139
dist/contextify.js vendored Normal file

File diff suppressed because it is too large Load Diff

83
dist/fixasync.js vendored Normal file
View File

@ -0,0 +1,83 @@
'use strict';
// eslint-disable-next-line no-invalid-this, no-shadow
const {GeneratorFunction, AsyncFunction, AsyncGeneratorFunction, global, internal, host, hook} = this;
const {Contextify, Decontextify} = internal;
// eslint-disable-next-line no-shadow
const {Function, eval: eval_, Promise, Object, Reflect} = global;
const {getOwnPropertyDescriptor, defineProperty, assign} = Object;
const {apply: rApply, construct: rConstruct} = Reflect;
const FunctionHandler = {
__proto__: null,
apply(target, thiz, args) {
const type = this.type;
args = Decontextify.arguments(args);
try {
args = Contextify.value(hook(type, args));
} catch (e) {
throw Contextify.value(e);
}
return rApply(target, thiz, args);
},
construct(target, args, newTarget) {
const type = this.type;
args = Decontextify.arguments(args);
try {
args = Contextify.value(hook(type, args));
} catch (e) {
throw Contextify.value(e);
}
return rConstruct(target, args, newTarget);
}
};
function makeCheckFunction(type) {
return assign({
__proto__: null,
type
}, FunctionHandler);
}
function override(obj, prop, value) {
const desc = getOwnPropertyDescriptor(obj, prop);
desc.value = value;
defineProperty(obj, prop, desc);
}
const proxiedFunction = new host.Proxy(Function, makeCheckFunction('function'));
override(Function.prototype, 'constructor', proxiedFunction);
if (GeneratorFunction) {
Object.setPrototypeOf(GeneratorFunction, proxiedFunction);
override(GeneratorFunction.prototype, 'constructor', new host.Proxy(GeneratorFunction, makeCheckFunction('generator_function')));
}
if (AsyncFunction) {
Object.setPrototypeOf(AsyncFunction, proxiedFunction);
override(AsyncFunction.prototype, 'constructor', new host.Proxy(AsyncFunction, makeCheckFunction('async_function')));
}
if (AsyncGeneratorFunction) {
Object.setPrototypeOf(AsyncGeneratorFunction, proxiedFunction);
override(AsyncGeneratorFunction.prototype, 'constructor', new host.Proxy(AsyncGeneratorFunction, makeCheckFunction('async_generator_function')));
}
global.Function = proxiedFunction;
global.eval = new host.Proxy(eval_, makeCheckFunction('eval'));
if (Promise) {
Promise.prototype.then = new host.Proxy(Promise.prototype.then, makeCheckFunction('promise_then'));
// This seems not to work, and will produce
// UnhandledPromiseRejectionWarning: TypeError: Method Promise.prototype.then called on incompatible receiver [object Object].
// This is likely caused since the host.Promise.prototype.then cannot use the VM Proxy object.
// Contextify.connect(host.Promise.prototype.then, Promise.prototype.then);
if (Promise.prototype.finally) {
Promise.prototype.finally = new host.Proxy(Promise.prototype.finally, makeCheckFunction('promise_finally'));
// Contextify.connect(host.Promise.prototype.finally, Promise.prototype.finally);
}
if (Promise.prototype.catch) {
Promise.prototype.catch = new host.Proxy(Promise.prototype.catch, makeCheckFunction('promise_catch'));
// Contextify.connect(host.Promise.prototype.catch, Promise.prototype.catch);
}
}

138
dist/index.js vendored

File diff suppressed because one or more lines are too long

682
dist/sandbox.js vendored Normal file
View File

@ -0,0 +1,682 @@
/* eslint-disable no-shadow, no-invalid-this */
/* global vm, host, Contextify, Decontextify, VMError, options */
'use strict';
const {Script} = host.require('vm');
const fs = host.require('fs');
const pa = host.require('path');
const BUILTIN_MODULES = host.process.binding('natives');
const parseJSON = JSON.parse;
const importModuleDynamically = () => {
// We can't throw an error object here because since vm.Script doesn't store a context, we can't properly contextify that error object.
// eslint-disable-next-line no-throw-literal
throw 'Dynamic imports are not allowed.';
};
/**
* @param {Object} host Hosts's internal objects.
*/
return ((vm, host) => {
'use strict';
const global = this;
const TIMERS = new host.WeakMap(); // Contains map of timers created inside sandbox
const BUILTINS = {__proto__: null};
const CACHE = {__proto__: null};
const EXTENSIONS = {
__proto__: null,
['.json'](module, filename) {
try {
const code = fs.readFileSync(filename, 'utf8');
module.exports = parseJSON(code);
} catch (e) {
throw Contextify.value(e);
}
},
['.node'](module, filename) {
if (vm.options.require.context === 'sandbox') throw new VMError('Native modules can be required only with context set to \'host\'.');
try {
module.exports = Contextify.readonly(host.require(filename));
} catch (e) {
throw Contextify.value(e);
}
}
};
for (let i = 0; i < vm.options.sourceExtensions.length; i++) {
const ext = vm.options.sourceExtensions[i];
EXTENSIONS['.' + ext] = (module, filename, dirname) => {
if (vm.options.require.context !== 'sandbox') {
try {
module.exports = Contextify.readonly(host.require(filename));
} catch (e) {
throw Contextify.value(e);
}
} else {
let script;
try {
// Load module
let contents = fs.readFileSync(filename, 'utf8');
contents = vm._compiler(contents, filename);
const code = host.STRICT_MODULE_PREFIX + contents + host.MODULE_SUFFIX;
const ccode = vm._hook('run', [code]);
// Precompile script
script = new Script(ccode, {
__proto__: null,
filename: filename || 'vm.js',
displayErrors: false,
importModuleDynamically
});
} catch (ex) {
throw Contextify.value(ex);
}
const closure = script.runInContext(global, {
__proto__: null,
filename: filename || 'vm.js',
displayErrors: false,
importModuleDynamically
});
// run the script
closure(module.exports, module.require, module, filename, dirname);
}
};
}
const _parseExternalOptions = (options) => {
if (host.Array.isArray(options)) {
return {
__proto__: null,
external: options,
transitive: false
};
}
return {
__proto__: null,
external: options.modules,
transitive: options.transitive
};
};
/**
* Resolve filename.
*/
const _resolveFilename = (path) => {
if (!path) return null;
let hasPackageJson;
try {
path = pa.resolve(path);
const exists = fs.existsSync(path);
const isdir = exists ? fs.statSync(path).isDirectory() : false;
// direct file match
if (exists && !isdir) return path;
// load as file
for (let i = 0; i < vm.options.sourceExtensions.length; i++) {
const ext = vm.options.sourceExtensions[i];
if (fs.existsSync(`${path}.${ext}`)) return `${path}.${ext}`;
}
if (fs.existsSync(`${path}.json`)) return `${path}.json`;
if (fs.existsSync(`${path}.node`)) return `${path}.node`;
// load as module
hasPackageJson = fs.existsSync(`${path}/package.json`);
} catch (e) {
throw Contextify.value(e);
}
if (hasPackageJson) {
let pkg;
try {
pkg = fs.readFileSync(`${path}/package.json`, 'utf8');
} catch (e) {
throw Contextify.value(e);
}
try {
pkg = parseJSON(pkg);
} catch (ex) {
throw new VMError(`Module '${path}' has invalid package.json`, 'EMODULEINVALID');
}
let main;
if (pkg && pkg.main) {
main = _resolveFilename(`${path}/${pkg.main}`);
if (!main) main = _resolveFilename(`${path}/index`);
} else {
main = _resolveFilename(`${path}/index`);
}
return main;
}
// load as directory
try {
for (let i = 0; i < vm.options.sourceExtensions.length; i++) {
const ext = vm.options.sourceExtensions[i];
if (fs.existsSync(`${path}/index.${ext}`)) return `${path}/index.${ext}`;
}
if (fs.existsSync(`${path}/index.json`)) return `${path}/index.json`;
if (fs.existsSync(`${path}/index.node`)) return `${path}/index.node`;
} catch (e) {
throw Contextify.value(e);
}
return null;
};
/**
* Builtin require.
*/
const _requireBuiltin = (moduleName) => {
if (moduleName === 'buffer') return ({Buffer});
if (BUILTINS[moduleName]) return BUILTINS[moduleName].exports; // Only compiled builtins are stored here
if (moduleName === 'util') {
return Contextify.readonly(host.require(moduleName), {
// Allows VM context to use util.inherits
__proto__: null,
inherits: (ctor, superCtor) => {
ctor.super_ = superCtor;
Object.setPrototypeOf(ctor.prototype, superCtor.prototype);
}
});
}
if (moduleName === 'events' || moduleName === 'internal/errors') {
let script;
try {
script = new Script(`(function (exports, require, module, process, internalBinding) {
'use strict';
const primordials = global;
${BUILTIN_MODULES[moduleName]}
\n
});`, {
filename: `${moduleName}.vm.js`
});
} catch (e) {
throw Contextify.value(e);
}
// setup module scope
const module = BUILTINS[moduleName] = {
exports: {},
require: _requireBuiltin
};
// run script
try {
// FIXME binding should be contextified
script.runInContext(global)(module.exports, module.require, module, host.process, host.process.binding);
} catch (e) {
// e could be from inside or outside of sandbox
throw new VMError(`Error loading '${moduleName}'`);
}
return module.exports;
}
return Contextify.readonly(host.require(moduleName));
};
/**
* Prepare require.
*/
const _prepareRequire = (currentDirname, parentAllowsTransitive = false) => {
const _require = moduleName => {
let requireObj;
try {
const optionsObj = vm.options;
if (optionsObj.nesting && moduleName === 'vm2') return {VM: Contextify.readonly(host.VM), NodeVM: Contextify.readonly(host.NodeVM)};
requireObj = optionsObj.require;
} catch (e) {
throw Contextify.value(e);
}
if (!requireObj) throw new VMError(`Access denied to require '${moduleName}'`, 'EDENIED');
if (moduleName == null) throw new VMError("Module '' not found.", 'ENOTFOUND');
if (typeof moduleName !== 'string') throw new VMError(`Invalid module name '${moduleName}'`, 'EINVALIDNAME');
let filename;
let allowRequireTransitive = false;
// Mock?
try {
const {mock} = requireObj;
if (mock) {
const mockModule = mock[moduleName];
if (mockModule) {
return Contextify.readonly(mockModule);
}
}
} catch (e) {
throw Contextify.value(e);
}
// Builtin?
if (BUILTIN_MODULES[moduleName]) {
let allowed;
try {
const builtinObj = requireObj.builtin;
if (host.Array.isArray(builtinObj)) {
if (builtinObj.indexOf('*') >= 0) {
allowed = builtinObj.indexOf(`-${moduleName}`) === -1;
} else {
allowed = builtinObj.indexOf(moduleName) >= 0;
}
} else if (builtinObj) {
allowed = builtinObj[moduleName];
} else {
allowed = false;
}
} catch (e) {
throw Contextify.value(e);
}
if (!allowed) throw new VMError(`Access denied to require '${moduleName}'`, 'EDENIED');
return _requireBuiltin(moduleName);
}
// External?
let externalObj;
try {
externalObj = requireObj.external;
} catch (e) {
throw Contextify.value(e);
}
if (!externalObj) throw new VMError(`Access denied to require '${moduleName}'`, 'EDENIED');
if (/^(\.|\.\/|\.\.\/)/.exec(moduleName)) {
// Module is relative file, e.g. ./script.js or ../script.js
if (!currentDirname) throw new VMError('You must specify script path to load relative modules.', 'ENOPATH');
filename = _resolveFilename(`${currentDirname}/${moduleName}`);
} else if (/^(\/|\\|[a-zA-Z]:\\)/.exec(moduleName)) {
// Module is absolute file, e.g. /script.js or //server/script.js or C:\script.js
filename = _resolveFilename(moduleName);
} else {
// Check node_modules in path
if (!currentDirname) throw new VMError('You must specify script path to load relative modules.', 'ENOPATH');
if (typeof externalObj === 'object') {
let isWhitelisted;
try {
const { external, transitive } = _parseExternalOptions(externalObj);
isWhitelisted = external.some(ext => host.helpers.match(ext, moduleName)) || (transitive && parentAllowsTransitive);
} catch (e) {
throw Contextify.value(e);
}
if (!isWhitelisted) {
throw new VMError(`The module '${moduleName}' is not whitelisted in VM.`, 'EDENIED');
}
allowRequireTransitive = true;
}
// FIXME the paths array has side effects
const paths = currentDirname.split(pa.sep);
while (paths.length) {
const path = paths.join(pa.sep);
// console.log moduleName, "#{path}#{pa.sep}node_modules#{pa.sep}#{moduleName}"
filename = _resolveFilename(`${path}${pa.sep}node_modules${pa.sep}${moduleName}`);
if (filename) break;
paths.pop();
}
}
if (!filename) {
let resolveFunc;
try {
resolveFunc = requireObj.resolve;
} catch (e) {
throw Contextify.value(e);
}
if (resolveFunc) {
let resolved;
try {
resolved = requireObj.resolve(moduleName, currentDirname);
} catch (e) {
throw Contextify.value(e);
}
filename = _resolveFilename(resolved);
}
}
if (!filename) throw new VMError(`Cannot find module '${moduleName}'`, 'ENOTFOUND');
// return cache whenever possible
if (CACHE[filename]) return CACHE[filename].exports;
const dirname = pa.dirname(filename);
const extname = pa.extname(filename);
let allowedModule = true;
try {
const rootObj = requireObj.root;
if (rootObj) {
const rootPaths = host.Array.isArray(rootObj) ? rootObj : host.Array.of(rootObj);
allowedModule = rootPaths.some(path => host.String.prototype.startsWith.call(dirname, pa.resolve(path)));
}
} catch (e) {
throw Contextify.value(e);
}
if (!allowedModule) {
throw new VMError(`Module '${moduleName}' is not allowed to be required. The path is outside the border!`, 'EDENIED');
}
const module = CACHE[filename] = {
filename,
exports: {},
require: _prepareRequire(dirname, allowRequireTransitive)
};
// lookup extensions
if (EXTENSIONS[extname]) {
EXTENSIONS[extname](module, filename, dirname);
return module.exports;
}
throw new VMError(`Failed to load '${moduleName}': Unknown type.`, 'ELOADFAIL');
};
return _require;
};
/**
* Prepare sandbox.
*/
// This is a function and not an arrow function, since the original is also a function
global.setTimeout = function setTimeout(callback, delay, ...args) {
if (typeof callback !== 'function') throw new TypeError('"callback" argument must be a function');
let tmr;
try {
tmr = host.setTimeout(Decontextify.value(() => {
// FIXME ...args has side effects
callback(...args);
}), Decontextify.value(delay));
} catch (e) {
throw Contextify.value(e);
}
const local = Contextify.value(tmr);
TIMERS.set(local, tmr);
return local;
};
global.setInterval = function setInterval(callback, interval, ...args) {
if (typeof callback !== 'function') throw new TypeError('"callback" argument must be a function');
let tmr;
try {
tmr = host.setInterval(Decontextify.value(() => {
// FIXME ...args has side effects
callback(...args);
}), Decontextify.value(interval));
} catch (e) {
throw Contextify.value(e);
}
const local = Contextify.value(tmr);
TIMERS.set(local, tmr);
return local;
};
global.setImmediate = function setImmediate(callback, ...args) {
if (typeof callback !== 'function') throw new TypeError('"callback" argument must be a function');
let tmr;
try {
tmr = host.setImmediate(Decontextify.value(() => {
// FIXME ...args has side effects
callback(...args);
}));
} catch (e) {
throw Contextify.value(e);
}
const local = Contextify.value(tmr);
TIMERS.set(local, tmr);
return local;
};
global.clearTimeout = function clearTimeout(local) {
try {
host.clearTimeout(TIMERS.get(local));
} catch (e) {
throw Contextify.value(e);
}
};
global.clearInterval = function clearInterval(local) {
try {
host.clearInterval(TIMERS.get(local));
} catch (e) {
throw Contextify.value(e);
}
};
global.clearImmediate = function clearImmediate(local) {
try {
host.clearImmediate(TIMERS.get(local));
} catch (e) {
throw Contextify.value(e);
}
};
function addListener(name, handler) {
if (name !== 'beforeExit' && name !== 'exit') {
throw new Error(`Access denied to listen for '${name}' event.`);
}
try {
host.process.on(name, Decontextify.value(handler));
} catch (e) {
throw Contextify.value(e);
}
return this;
}
const {argv: optionArgv, env: optionsEnv} = options;
// FIXME wrong class structure
global.process = {
argv: optionArgv !== undefined ? Contextify.value(optionArgv) : [],
title: host.process.title,
version: host.process.version,
versions: Contextify.readonly(host.process.versions),
arch: host.process.arch,
platform: host.process.platform,
env: optionsEnv !== undefined ? Contextify.value(optionsEnv) : {},
pid: host.process.pid,
features: Contextify.readonly(host.process.features),
nextTick: function nextTick(callback, ...args) {
if (typeof callback !== 'function') {
throw new Error('Callback must be a function.');
}
try {
host.process.nextTick(Decontextify.value(() => {
// FIXME ...args has side effects
callback(...args);
}));
} catch (e) {
throw Contextify.value(e);
}
},
hrtime: function hrtime(time) {
try {
return Contextify.value(host.process.hrtime(Decontextify.value(time)));
} catch (e) {
throw Contextify.value(e);
}
},
cwd: function cwd() {
try {
return Contextify.value(host.process.cwd());
} catch (e) {
throw Contextify.value(e);
}
},
addListener,
on: addListener,
once: function once(name, handler) {
if (name !== 'beforeExit' && name !== 'exit') {
throw new Error(`Access denied to listen for '${name}' event.`);
}
try {
host.process.once(name, Decontextify.value(handler));
} catch (e) {
throw Contextify.value(e);
}
return this;
},
listeners: function listeners(name) {
if (name !== 'beforeExit' && name !== 'exit') {
// Maybe add ({__proto__:null})[name] to throw when name fails in https://tc39.es/ecma262/#sec-topropertykey.
return [];
}
// Filter out listeners, which were not created in this sandbox
try {
return Contextify.value(host.process.listeners(name).filter(listener => Contextify.isVMProxy(listener)));
} catch (e) {
throw Contextify.value(e);
}
},
removeListener: function removeListener(name, handler) {
if (name !== 'beforeExit' && name !== 'exit') {
return this;
}
try {
host.process.removeListener(name, Decontextify.value(handler));
} catch (e) {
throw Contextify.value(e);
}
return this;
},
umask: function umask() {
if (arguments.length) {
throw new Error('Access denied to set umask.');
}
try {
return Contextify.value(host.process.umask());
} catch (e) {
throw Contextify.value(e);
}
}
};
if (vm.options.console === 'inherit') {
global.console = Contextify.readonly(host.console);
} else if (vm.options.console === 'redirect') {
global.console = {
debug(...args) {
try {
// FIXME ...args has side effects
vm.emit('console.debug', ...Decontextify.arguments(args));
} catch (e) {
throw Contextify.value(e);
}
},
log(...args) {
try {
// FIXME ...args has side effects
vm.emit('console.log', ...Decontextify.arguments(args));
} catch (e) {
throw Contextify.value(e);
}
},
info(...args) {
try {
// FIXME ...args has side effects
vm.emit('console.info', ...Decontextify.arguments(args));
} catch (e) {
throw Contextify.value(e);
}
},
warn(...args) {
try {
// FIXME ...args has side effects
vm.emit('console.warn', ...Decontextify.arguments(args));
} catch (e) {
throw Contextify.value(e);
}
},
error(...args) {
try {
// FIXME ...args has side effects
vm.emit('console.error', ...Decontextify.arguments(args));
} catch (e) {
throw Contextify.value(e);
}
},
dir(...args) {
try {
vm.emit('console.dir', ...Decontextify.arguments(args));
} catch (e) {
throw Contextify.value(e);
}
},
time() {},
timeEnd() {},
trace(...args) {
try {
// FIXME ...args has side effects
vm.emit('console.trace', ...Decontextify.arguments(args));
} catch (e) {
throw Contextify.value(e);
}
}
};
}
/*
Return contextified require.
*/
return _prepareRequire;
})(vm, host);

11
jest.config.js Normal file
View File

@ -0,0 +1,11 @@
module.exports = {
clearMocks: true,
moduleFileExtensions: ['js', 'ts'],
testEnvironment: 'node',
testMatch: ['**/*.test.ts'],
testRunner: 'jest-circus/runner',
transform: {
'^.+\\.ts$': 'ts-jest'
},
verbose: true
}

View File

@ -1,16 +0,0 @@
import type { JestConfigWithTsJest } from 'ts-jest';
const config: JestConfigWithTsJest = {
preset: 'ts-jest/presets/default-esm',
clearMocks: true,
moduleFileExtensions: ['js', 'ts'],
testEnvironment: 'node',
testMatch: ['**/*.test.ts'],
testRunner: 'jest-circus/runner',
transform: {
'^.+\\.ts$': 'ts-jest',
},
verbose: true,
};
export default config;

9136
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,16 +1,14 @@
{ {
"name": "action-gh-release", "name": "action-gh-release",
"version": "2.2.1", "version": "0.1.14",
"private": true, "private": true,
"description": "GitHub Action for creating GitHub Releases", "description": "GitHub Action for creating GitHub Releases",
"main": "lib/main.js", "main": "lib/main.js",
"scripts": { "scripts": {
"build": "ncc build src/main.ts --minify", "build": "ncc build src/main.ts --minify",
"build-debug": "ncc build src/main.ts --v8-cache --source-map",
"test": "jest", "test": "jest",
"fmt": "prettier --write \"src/**/*.ts\" \"__tests__/**/*.ts\"", "fmt": "prettier --write \"src/**/*.ts\" \"__tests__/**/*.ts\"",
"fmtcheck": "prettier --check \"src/**/*.ts\" \"__tests__/**/*.ts\"", "fmtcheck": "prettier --check \"src/**/*.ts\" \"__tests__/**/*.ts\""
"updatetag": "git tag -d v2 && git push origin :v2 && git tag -a v2 -m '' && git push origin v2"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -20,26 +18,28 @@
"actions" "actions"
], ],
"author": "softprops", "author": "softprops",
"license": "MIT",
"dependencies": { "dependencies": {
"@actions/core": "^1.11.1", "@octokit/action": "^3.18.0",
"@actions/github": "^6.0.0", "@actions/core": "^1.4.0",
"@octokit/plugin-retry": "^7.2.0", "@octokit/plugin-retry": "^3.0.9",
"@octokit/plugin-throttling": "^9.6.1", "@octokit/plugin-throttling": "^3.5.1",
"glob": "^11.0.1", "glob": "^7.1.6",
"mime": "^3.0.0" "mime": "^2.4.4",
"node-fetch": "^2.6.1"
}, },
"devDependencies": { "devDependencies": {
"@types/glob": "^8.1.0", "@types/glob": "^7.1.1",
"@types/jest": "^29.5.14", "@types/jest": "^24.0.25",
"@types/mime": "^3.0.1", "@types/mime": "^2.0.1",
"@types/node": "^22.14.0", "@types/node": "^12.12.24",
"@vercel/ncc": "^0.38.3", "@types/node-fetch": "^2.5.12",
"jest": "^29.3.1", "@vercel/ncc": "^0.33.0",
"jest-circus": "^29.3.1", "jest": "^24.9.0",
"prettier": "3.5.3", "jest-circus": "^24.9.0",
"ts-jest": "^29.3.1", "prettier": "1.19.1",
"ts-node": "^10.9.2", "ts-jest": "^24.2.0",
"typescript": "^5.8.3", "typescript": "^3.7.4",
"typescript-formatter": "^7.2.2" "typescript-formatter": "^7.2.2"
} }
} }

17
release.sh Executable file
View File

@ -0,0 +1,17 @@
#!/bin/bash
# actions requires a node_modules dir https://github.com/actions/toolkit/blob/master/docs/javascript-action.md#publish-a-releasesv1-action
# but its recommended not to check these in https://github.com/actions/toolkit/blob/master/docs/action-versioning.md#recommendations
# as such the following hack is how we dill with it
if [[ $# -ne 1 ]]; then
echo "please pass a release version. i.e. $0 v1"
exit 1
fi
git checkout -b releases/$1 # If this branch already exists, omit the -b flag
rm -rf node_modules
sed -i '/node_modules/d' .gitignore # Bash command that removes node_modules from .gitignore
npm install --production
git add node_modules -f .gitignore
git commit -m node_modules
git push origin releases/$1

View File

@ -1,16 +1,17 @@
import { GitHub } from "@actions/github/lib/utils"; import fetch from "node-fetch";
import { Config, isTag, releaseBody, alignAssetName } from "./util"; import { Octokit } from "@octokit/action";
import { statSync } from "fs"; import { Config, isTag, releaseBody } from "./util";
import { open } from "fs/promises"; import { statSync, readFileSync } from "fs";
import { getType } from "mime"; import { getType } from "mime";
import { basename } from "path"; import { basename } from "path";
type GitHub = InstanceType<typeof GitHub>; type GitHub = InstanceType<typeof Octokit>;
export interface ReleaseAsset { export interface ReleaseAsset {
name: string; name: string;
mime: string; mime: string;
size: number; size: number;
data: Buffer;
} }
export interface Release { export interface Release {
@ -44,7 +45,6 @@ export interface Releaser {
target_commitish: string | undefined; target_commitish: string | undefined;
discussion_category_name: string | undefined; discussion_category_name: string | undefined;
generate_release_notes: boolean | undefined; generate_release_notes: boolean | undefined;
make_latest: "true" | "false" | "legacy" | undefined;
}): Promise<{ data: Release }>; }): Promise<{ data: Release }>;
updateRelease(params: { updateRelease(params: {
@ -59,7 +59,6 @@ export interface Releaser {
prerelease: boolean | undefined; prerelease: boolean | undefined;
discussion_category_name: string | undefined; discussion_category_name: string | undefined;
generate_release_notes: boolean | undefined; generate_release_notes: boolean | undefined;
make_latest: "true" | "false" | "legacy" | undefined;
}): Promise<{ data: Release }>; }): Promise<{ data: Release }>;
allReleases(params: { allReleases(params: {
@ -93,15 +92,7 @@ export class GitHubReleaser implements Releaser {
target_commitish: string | undefined; target_commitish: string | undefined;
discussion_category_name: string | undefined; discussion_category_name: string | undefined;
generate_release_notes: boolean | undefined; generate_release_notes: boolean | undefined;
make_latest: "true" | "false" | "legacy" | undefined;
}): Promise<{ data: Release }> { }): Promise<{ data: Release }> {
if (
typeof params.make_latest === "string" &&
!["true", "false", "legacy"].includes(params.make_latest)
) {
params.make_latest = undefined;
}
return this.github.rest.repos.createRelease(params); return this.github.rest.repos.createRelease(params);
} }
@ -117,15 +108,7 @@ export class GitHubReleaser implements Releaser {
prerelease: boolean | undefined; prerelease: boolean | undefined;
discussion_category_name: string | undefined; discussion_category_name: string | undefined;
generate_release_notes: boolean | undefined; generate_release_notes: boolean | undefined;
make_latest: "true" | "false" | "legacy" | undefined;
}): Promise<{ data: Release }> { }): Promise<{ data: Release }> {
if (
typeof params.make_latest === "string" &&
!["true", "false", "legacy"].includes(params.make_latest)
) {
params.make_latest = undefined;
}
return this.github.rest.repos.updateRelease(params); return this.github.rest.repos.updateRelease(params);
} }
@ -135,7 +118,7 @@ export class GitHubReleaser implements Releaser {
}): AsyncIterableIterator<{ data: Release[] }> { }): AsyncIterableIterator<{ data: Release[] }> {
const updatedParams = { per_page: 100, ...params }; const updatedParams = { per_page: 100, ...params };
return this.github.paginate.iterator( return this.github.paginate.iterator(
this.github.rest.repos.listReleases.endpoint.merge(updatedParams), this.github.rest.repos.listReleases.endpoint.merge(updatedParams)
); );
} }
} }
@ -145,6 +128,7 @@ export const asset = (path: string): ReleaseAsset => {
name: basename(path), name: basename(path),
mime: mimeOrDefault(path), mime: mimeOrDefault(path),
size: statSync(path).size, size: statSync(path).size,
data: readFileSync(path)
}; };
}; };
@ -157,58 +141,48 @@ export const upload = async (
github: GitHub, github: GitHub,
url: string, url: string,
path: string, path: string,
currentAssets: Array<{ id: number; name: string }>, currentAssets: Array<{ id: number; name: string }>
): Promise<any> => { ): Promise<any> => {
const [owner, repo] = config.github_repository.split("/"); const [owner, repo] = config.github_repository.split("/");
const { name, mime, size } = asset(path); const { name, size, mime, data: body } = asset(path);
const currentAsset = currentAssets.find( const currentAsset = currentAssets.find(
// note: GitHub renames asset filenames that have special characters, non-alphanumeric characters, and leading or trailing periods. The "List release assets" endpoint lists the renamed filenames. ({ name: currentName }) => currentName == name
// due to this renaming we need to be mindful when we compare the file name we're uploading with a name github may already have rewritten for logical comparison
// see https://docs.github.com/en/rest/releases/assets?apiVersion=2022-11-28#upload-a-release-asset
({ name: currentName }) => currentName == alignAssetName(name),
); );
if (currentAsset) { if (currentAsset) {
console.log(`♻️ Deleting previously uploaded asset ${name}...`); console.log(`♻️ Deleting previously uploaded asset ${name}...`);
await github.rest.repos.deleteReleaseAsset({ await github.rest.repos.deleteReleaseAsset({
asset_id: currentAsset.id || 1, asset_id: currentAsset.id || 1,
owner, owner,
repo, repo
}); });
} }
console.log(`⬆️ Uploading ${name}...`); console.log(`⬆️ Uploading ${name}...`);
const endpoint = new URL(url); const endpoint = new URL(url);
endpoint.searchParams.append("name", name); endpoint.searchParams.append("name", name);
const fh = await open(path); const resp = await fetch(endpoint, {
try { headers: {
const resp = await github.request({ "content-length": `${size}`,
method: "POST", "content-type": mime,
url: endpoint.toString(), authorization: `token ${config.github_token}`
headers: { },
"content-length": `${size}`, method: "POST",
"content-type": mime, body
authorization: `token ${config.github_token}`, });
}, const json = await resp.json();
data: fh.readableWebStream({ type: "bytes" }), if (resp.status !== 201) {
}); throw new Error(
const json = resp.data; `Failed to upload release asset ${name}. received status code ${
if (resp.status !== 201) { resp.status
throw new Error( }\n${json.message}\n${JSON.stringify(json.errors)}`
`Failed to upload release asset ${name}. received status code ${ );
resp.status
}\n${json.message}\n${JSON.stringify(json.errors)}`,
);
}
console.log(`✅ Uploaded ${name}`);
return json;
} finally {
await fh.close();
} }
return json;
}; };
export const release = async ( export const release = async (
config: Config, config: Config,
releaser: Releaser, releaser: Releaser,
maxRetries: number = 3, maxRetries: number = 3
): Promise<Release> => { ): Promise<Release> => {
if (maxRetries <= 0) { if (maxRetries <= 0) {
console.log(`❌ Too many retries. Aborting...`); console.log(`❌ Too many retries. Aborting...`);
@ -225,75 +199,55 @@ export const release = async (
const discussion_category_name = config.input_discussion_category_name; const discussion_category_name = config.input_discussion_category_name;
const generate_release_notes = config.input_generate_release_notes; const generate_release_notes = config.input_generate_release_notes;
try { try {
// you can't get an existing draft by tag // you can't get a an existing draft by tag
// so we must find one in the list of all releases // so we must find one in the list of all releases
let _release: Release | undefined = undefined; if (config.input_draft) {
for await (const response of releaser.allReleases({ for await (const response of releaser.allReleases({
owner, owner,
repo, repo
})) { })) {
_release = response.data.find((release) => release.tag_name === tag); let release = response.data.find(release => release.tag_name === tag);
if (_release !== undefined) { if (release) {
break; return release;
}
} }
} }
if (_release === undefined) { let existingRelease = await releaser.getReleaseByTag({
return await createRelease( owner,
tag, repo,
config, tag
releaser, });
owner,
repo,
discussion_category_name,
generate_release_notes,
maxRetries,
);
}
let existingRelease: Release = _release!; const release_id = existingRelease.data.id;
console.log(
`Found release ${existingRelease.name} (with id=${existingRelease.id})`,
);
const release_id = existingRelease.id;
let target_commitish: string; let target_commitish: string;
if ( if (
config.input_target_commitish && config.input_target_commitish &&
config.input_target_commitish !== existingRelease.target_commitish config.input_target_commitish !== existingRelease.data.target_commitish
) { ) {
console.log( console.log(
`Updating commit from "${existingRelease.target_commitish}" to "${config.input_target_commitish}"`, `Updating commit from "${existingRelease.data.target_commitish}" to "${config.input_target_commitish}"`
); );
target_commitish = config.input_target_commitish; target_commitish = config.input_target_commitish;
} else { } else {
target_commitish = existingRelease.target_commitish; target_commitish = existingRelease.data.target_commitish;
} }
const tag_name = tag; const tag_name = tag;
const name = config.input_name || existingRelease.name || tag; const name = config.input_name || existingRelease.data.name || tag;
// revisit: support a new body-concat-strategy input for accumulating // revisit: support a new body-concat-strategy input for accumulating
// body parts as a release gets updated. some users will likely want this while // body parts as a release gets updated. some users will likely want this while
// others won't previously this was duplicating content for most which // others won't previously this was duplicating content for most which
// no one wants // no one wants
const workflowBody = releaseBody(config) || ""; let body = releaseBody(config) || existingRelease.data.body || "";
const existingReleaseBody = existingRelease.body || "";
let body: string;
if (config.input_append_body && workflowBody && existingReleaseBody) {
body = existingReleaseBody + "\n" + workflowBody;
} else {
body = workflowBody || existingReleaseBody;
}
const draft = const draft =
config.input_draft !== undefined config.input_draft !== undefined
? config.input_draft ? config.input_draft
: existingRelease.draft; : existingRelease.data.draft;
const prerelease = const prerelease =
config.input_prerelease !== undefined config.input_prerelease !== undefined
? config.input_prerelease ? config.input_prerelease
: existingRelease.prerelease; : existingRelease.data.prerelease;
const make_latest = config.input_make_latest;
const release = await releaser.updateRelease({ const release = await releaser.updateRelease({
owner, owner,
@ -306,92 +260,54 @@ export const release = async (
draft, draft,
prerelease, prerelease,
discussion_category_name, discussion_category_name,
generate_release_notes, generate_release_notes
make_latest,
}); });
return release.data; return release.data;
} catch (error) { } catch (error) {
if (error.status !== 404) { if (error.status === 404) {
const tag_name = tag;
const name = config.input_name || tag;
const body = releaseBody(config);
const draft = config.input_draft;
const prerelease = config.input_prerelease;
const target_commitish = config.input_target_commitish;
let commitMessage: string = "";
if (target_commitish) {
commitMessage = ` using commit "${target_commitish}"`;
}
console.log( console.log(
`⚠️ Unexpected error fetching GitHub release for tag ${config.github_ref}: ${error}`, `👩‍🏭 Creating new GitHub release for tag ${tag_name}${commitMessage}...`
);
try {
let release = await releaser.createRelease({
owner,
repo,
tag_name,
name,
body,
draft,
prerelease,
target_commitish,
discussion_category_name,
generate_release_notes
});
return release.data;
} catch (error) {
// presume a race with competing metrix runs
console.log(
`⚠️ GitHub release failed with status: ${
error.status
}\n${JSON.stringify(
error.response.data.errors
)}\nretrying... (${maxRetries - 1} retries remaining)`
);
return release(config, releaser, maxRetries - 1);
}
} else {
console.log(
`⚠️ Unexpected error fetching GitHub release for tag ${config.github_ref}: ${error}`
); );
throw error; throw error;
} }
return await createRelease(
tag,
config,
releaser,
owner,
repo,
discussion_category_name,
generate_release_notes,
maxRetries,
);
} }
}; };
async function createRelease(
tag: string,
config: Config,
releaser: Releaser,
owner: string,
repo: string,
discussion_category_name: string | undefined,
generate_release_notes: boolean | undefined,
maxRetries: number,
) {
const tag_name = tag;
const name = config.input_name || tag;
const body = releaseBody(config);
const draft = config.input_draft;
const prerelease = config.input_prerelease;
const target_commitish = config.input_target_commitish;
const make_latest = config.input_make_latest;
let commitMessage: string = "";
if (target_commitish) {
commitMessage = ` using commit "${target_commitish}"`;
}
console.log(
`👩‍🏭 Creating new GitHub release for tag ${tag_name}${commitMessage}...`,
);
try {
let release = await releaser.createRelease({
owner,
repo,
tag_name,
name,
body,
draft,
prerelease,
target_commitish,
discussion_category_name,
generate_release_notes,
make_latest,
});
return release.data;
} catch (error) {
// presume a race with competing matrix runs
console.log(`⚠️ GitHub release failed with status: ${error.status}`);
console.log(`${JSON.stringify(error.response.data)}`);
switch (error.status) {
case 403:
console.log(
"Skip retry — your GitHub token/PAT does not have the required permission to create a release",
);
throw error;
case 404:
console.log("Skip retry - discussion category mismatch");
throw error;
case 422:
console.log("Skip retry - validation failed");
throw error;
}
console.log(`retrying... (${maxRetries - 1} retries remaining)`);
return release(config, releaser, maxRetries - 1);
}
}

View File

@ -3,12 +3,13 @@ import {
parseConfig, parseConfig,
isTag, isTag,
unmatchedPatterns, unmatchedPatterns,
uploadUrl, uploadUrl
} from "./util"; } from "./util";
import { release, upload, GitHubReleaser } from "./github"; import { release, upload, GitHubReleaser } from "./github";
import { getOctokit } from "@actions/github"; import { Octokit } from "@octokit/action";
import { setFailed, setOutput } from "@actions/core"; import { setFailed, setOutput } from "@actions/core";
import { retry } from "@octokit/plugin-retry";
import { throttling } from "@octokit/plugin-throttling";
import { env } from "process"; import { env } from "process";
async function run() { async function run() {
@ -23,29 +24,21 @@ async function run() {
} }
if (config.input_files) { if (config.input_files) {
const patterns = unmatchedPatterns(config.input_files); const patterns = unmatchedPatterns(config.input_files);
patterns.forEach((pattern) => { patterns.forEach(pattern =>
if (config.input_fail_on_unmatched_files) { console.warn(`🤔 Pattern '${pattern}' does not match any files.`)
throw new Error(`⚠️ Pattern '${pattern}' does not match any files.`); );
} else {
console.warn(`🤔 Pattern '${pattern}' does not match any files.`);
}
});
if (patterns.length > 0 && config.input_fail_on_unmatched_files) { if (patterns.length > 0 && config.input_fail_on_unmatched_files) {
throw new Error(`⚠️ There were unmatched files`); throw new Error(`⚠️ There were unmatched files`);
} }
} }
// const oktokit = GitHub.plugin( const OctokitWithPlugins = Octokit.plugin(retry, throttling);
// require("@octokit/plugin-throttling"), const gh = new OctokitWithPlugins({
// require("@octokit/plugin-retry") auth: config.github_token,
// );
const gh = getOctokit(config.github_token, {
//new oktokit(
throttle: { throttle: {
onRateLimit: (retryAfter, options) => { onRateLimit: (retryAfter, options) => {
console.warn( console.warn(
`Request quota exhausted for request ${options.method} ${options.url}`, `Request quota exhausted for request ${options.method} ${options.url}`
); );
if (options.request.retryCount === 0) { if (options.request.retryCount === 0) {
// only retries once // only retries once
@ -56,49 +49,34 @@ async function run() {
onAbuseLimit: (retryAfter, options) => { onAbuseLimit: (retryAfter, options) => {
// does not retry, only logs a warning // does not retry, only logs a warning
console.warn( console.warn(
`Abuse detected for request ${options.method} ${options.url}`, `Abuse detected for request ${options.method} ${options.url}`
); );
}, }
}, }
}); });
//); //);
const rel = await release(config, new GitHubReleaser(gh)); const rel = await release(config, new GitHubReleaser(gh));
if (config.input_files && config.input_files.length > 0) { if (config.input_files) {
const files = paths(config.input_files); const files = paths(config.input_files);
if (files.length == 0) { if (files.length == 0) {
if (config.input_fail_on_unmatched_files) { console.warn(`🤔 ${config.input_files} not include valid file.`);
throw new Error(
`⚠️ ${config.input_files} does not include a valid file.`,
);
} else {
console.warn(
`🤔 ${config.input_files} does not include a valid file.`,
);
}
} }
const currentAssets = rel.assets; const currentAssets = rel.assets;
const assets = await Promise.all(
const uploadFile = async (path) => { files.map(async path => {
const json = await upload( const json = await upload(
config, config,
gh, gh,
uploadUrl(rel.upload_url), uploadUrl(rel.upload_url),
path, path,
currentAssets, currentAssets
); );
delete json.uploader; delete json.uploader;
return json; return json;
}; })
).catch(error => {
let assets; throw error;
if (!config.input_preserve_order) { });
assets = await Promise.all(files.map(uploadFile));
} else {
assets = [];
for (const path of files) {
assets.push(await uploadFile(path));
}
}
setOutput("assets", assets); setOutput("assets", assets);
} }
console.log(`🎉 Release ready at ${rel.html_url}`); console.log(`🎉 Release ready at ${rel.html_url}`);

View File

@ -13,14 +13,11 @@ export interface Config {
input_body_path?: string; input_body_path?: string;
input_files?: string[]; input_files?: string[];
input_draft?: boolean; input_draft?: boolean;
input_preserve_order?: boolean;
input_prerelease?: boolean; input_prerelease?: boolean;
input_fail_on_unmatched_files?: boolean; input_fail_on_unmatched_files?: boolean;
input_target_commitish?: string; input_target_commitish?: string;
input_discussion_category_name?: string; input_discussion_category_name?: string;
input_generate_release_notes?: boolean; input_generate_release_notes?: boolean;
input_append_body?: boolean;
input_make_latest: "true" | "false" | "legacy" | undefined;
} }
export const uploadUrl = (url: string): string => { export const uploadUrl = (url: string): string => {
@ -46,9 +43,9 @@ export const parseInputFiles = (files: string): string[] => {
(acc, line) => (acc, line) =>
acc acc
.concat(line.split(",")) .concat(line.split(","))
.filter((pat) => pat) .filter(pat => pat)
.map((pat) => pat.trim()), .map(pat => pat.trim()),
[], []
); );
}; };
@ -63,9 +60,6 @@ export const parseConfig = (env: Env): Config => {
input_body_path: env.INPUT_BODY_PATH, input_body_path: env.INPUT_BODY_PATH,
input_files: parseInputFiles(env.INPUT_FILES || ""), input_files: parseInputFiles(env.INPUT_FILES || ""),
input_draft: env.INPUT_DRAFT ? env.INPUT_DRAFT === "true" : undefined, input_draft: env.INPUT_DRAFT ? env.INPUT_DRAFT === "true" : undefined,
input_preserve_order: env.INPUT_PRESERVE_ORDER
? env.INPUT_PRESERVE_ORDER == "true"
: undefined,
input_prerelease: env.INPUT_PRERELEASE input_prerelease: env.INPUT_PRERELEASE
? env.INPUT_PRERELEASE == "true" ? env.INPUT_PRERELEASE == "true"
: undefined, : undefined,
@ -73,25 +67,14 @@ export const parseConfig = (env: Env): Config => {
input_target_commitish: env.INPUT_TARGET_COMMITISH || undefined, input_target_commitish: env.INPUT_TARGET_COMMITISH || undefined,
input_discussion_category_name: input_discussion_category_name:
env.INPUT_DISCUSSION_CATEGORY_NAME || undefined, env.INPUT_DISCUSSION_CATEGORY_NAME || undefined,
input_generate_release_notes: env.INPUT_GENERATE_RELEASE_NOTES == "true", input_generate_release_notes: env.INPUT_GENERATE_RELEASE_NOTES == "true"
input_append_body: env.INPUT_APPEND_BODY == "true",
input_make_latest: parseMakeLatest(env.INPUT_MAKE_LATEST),
}; };
}; };
const parseMakeLatest = (
value: string | undefined,
): "true" | "false" | "legacy" | undefined => {
if (value === "true" || value === "false" || value === "legacy") {
return value;
}
return undefined;
};
export const paths = (patterns: string[]): string[] => { export const paths = (patterns: string[]): string[] => {
return patterns.reduce((acc: string[], pattern: string): string[] => { return patterns.reduce((acc: string[], pattern: string): string[] => {
return acc.concat( return acc.concat(
glob.sync(pattern).filter((path) => statSync(path).isFile()), glob.sync(pattern).filter(path => statSync(path).isFile())
); );
}, []); }, []);
}; };
@ -99,9 +82,9 @@ export const paths = (patterns: string[]): string[] => {
export const unmatchedPatterns = (patterns: string[]): string[] => { export const unmatchedPatterns = (patterns: string[]): string[] => {
return patterns.reduce((acc: string[], pattern: string): string[] => { return patterns.reduce((acc: string[], pattern: string): string[] => {
return acc.concat( return acc.concat(
glob.sync(pattern).filter((path) => statSync(path).isFile()).length == 0 glob.sync(pattern).filter(path => statSync(path).isFile()).length == 0
? [pattern] ? [pattern]
: [], : []
); );
}, []); }, []);
}; };
@ -109,7 +92,3 @@ export const unmatchedPatterns = (patterns: string[]): string[] => {
export const isTag = (ref: string): boolean => { export const isTag = (ref: string): boolean => {
return ref.startsWith("refs/tags/"); return ref.startsWith("refs/tags/");
}; };
export const alignAssetName = (assetName: string): string => {
return assetName.replace(/ /g, ".");
};

View File

@ -1,6 +1,5 @@
{ {
"compilerOptions": { "compilerOptions": {
"useUnknownInCatchVariables": false,
/* Basic Options */ /* Basic Options */
// "incremental": true, /* Enable incremental compilation */ // "incremental": true, /* Enable incremental compilation */
"target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
@ -60,5 +59,5 @@
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
}, },
"exclude": ["node_modules", "**/*.test.ts", "jest.config.ts"] "exclude": ["node_modules", "**/*.test.ts"]
} }