Compare commits
18 Commits
predicate@
...
predicate@
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
534423496e | ||
|
|
2f5f68fcc3 | ||
|
|
36d21cdc72 | ||
|
|
38c481ec87 | ||
|
|
d9763b28c9 | ||
|
|
1afe01eb23 | ||
|
|
a624c741b8 | ||
|
|
d4f0c27f8d | ||
|
|
ab147f15c3 | ||
|
|
41d694c98d | ||
|
|
0b5415aa25 | ||
|
|
817d650747 | ||
|
|
5d89d51206 | ||
|
|
f8e0f3ab00 | ||
|
|
e37c92d3ba | ||
|
|
d023f128e6 | ||
|
|
48e5743928 | ||
|
|
05284cc010 |
2
.github/dependabot.yml
vendored
2
.github/dependabot.yml
vendored
@@ -9,6 +9,8 @@ updates:
|
|||||||
update-types:
|
update-types:
|
||||||
- minor
|
- minor
|
||||||
- patch
|
- patch
|
||||||
|
ignore:
|
||||||
|
- dependency-name: "actions/attest-sbom"
|
||||||
|
|
||||||
- package-ecosystem: npm
|
- package-ecosystem: npm
|
||||||
directory: /
|
directory: /
|
||||||
|
|||||||
33
.github/workflows/ci.yml
vendored
33
.github/workflows/ci.yml
vendored
@@ -46,11 +46,12 @@ jobs:
|
|||||||
id: npm-ci-test
|
id: npm-ci-test
|
||||||
run: npm run ci-test
|
run: npm run ci-test
|
||||||
|
|
||||||
test-attest-sbom-with-local-sbom-file:
|
test-attest-sbom:
|
||||||
name: Test attest-sbom action with local sbom file
|
name: Test attest-sbom action with local sbom file
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
attestations: write
|
||||||
|
contents: read
|
||||||
id-token: write
|
id-token: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -69,31 +70,3 @@ jobs:
|
|||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Dump output
|
- name: Dump output
|
||||||
run: jq < ${{ steps.attest-sbom.outputs.bundle-path }}
|
run: jq < ${{ steps.attest-sbom.outputs.bundle-path }}
|
||||||
test-attest-sbom:
|
|
||||||
name: Test attest-sbom action
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
id-token: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
id: checkout
|
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
|
||||||
- name: Run attest-sbom with spdx format
|
|
||||||
uses: ./
|
|
||||||
with:
|
|
||||||
subject-digest: 'sha256:7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32'
|
|
||||||
subject-name: 'subject'
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
sbom-format: 'spdx'
|
|
||||||
- name: Run attest-sbom with cyclonedx format
|
|
||||||
id: attest-sbom
|
|
||||||
uses: ./
|
|
||||||
env:
|
|
||||||
INPUT_PRIVATE-SIGNING: 'true'
|
|
||||||
with:
|
|
||||||
subject-digest: 'sha256:7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32'
|
|
||||||
subject-name: 'subject'
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
sbom-format: 'cyclonedx'
|
|
||||||
|
|||||||
31
README.md
31
README.md
@@ -5,9 +5,8 @@ the [@actions/attest][1] package.
|
|||||||
|
|
||||||
Attestations bind some subject (a named artifact along with its digest) to a a
|
Attestations bind some subject (a named artifact along with its digest) to a a
|
||||||
Software Bill of Materials (SBOM) using the [in-toto][2] format. The action
|
Software Bill of Materials (SBOM) using the [in-toto][2] format. The action
|
||||||
accepts SBOMs which have been generated by external tools or can generate one
|
accepts SBOMs which have been generated by external tools. Provided SBOMs must
|
||||||
automatically by invoking the [anchore/sbom-action][3]. Externally generated
|
be in either the [SPDX][4] or [CycloneDX][5] JSON-serialized format.
|
||||||
SBOMs must be in either the [SPDX][4] or [CycloneDX][5] JSON-serialized format.
|
|
||||||
|
|
||||||
A verifiable signature is generated for the attestation using a short-lived
|
A verifiable signature is generated for the attestation using a short-lived
|
||||||
[Sigstore][6]-issued signing certificate. If the repository initiating the
|
[Sigstore][6]-issued signing certificate. If the repository initiating the
|
||||||
@@ -32,11 +31,11 @@ attest:
|
|||||||
```yaml
|
```yaml
|
||||||
permissions:
|
permissions:
|
||||||
id-token: write
|
id-token: write
|
||||||
contents: write # TODO: Update this
|
attestations: write
|
||||||
```
|
```
|
||||||
|
|
||||||
The `id-token` permission gives the action the ability to mint the OIDC token
|
The `id-token` permission gives the action the ability to mint the OIDC token
|
||||||
necessary to request a Sigstore signing certificate. The `contents`
|
necessary to request a Sigstore signing certificate. The `attestations`
|
||||||
permission is necessary to persist the attestation.
|
permission is necessary to persist the attestation.
|
||||||
|
|
||||||
1. Add the following to your workflow after your artifact has been built:
|
1. Add the following to your workflow after your artifact has been built:
|
||||||
@@ -45,12 +44,12 @@ attest:
|
|||||||
- uses: actions/attest-sbom@v1
|
- uses: actions/attest-sbom@v1
|
||||||
with:
|
with:
|
||||||
subject-path: '<PATH TO ARTIFACT>'
|
subject-path: '<PATH TO ARTIFACT>'
|
||||||
|
sbom-path: '<PATH TO SBOM>'
|
||||||
```
|
```
|
||||||
|
|
||||||
The `subject-path` parameter should identity the artifact for which you want
|
The `subject-path` parameter should identity the artifact for which you want
|
||||||
to generate an SBOM attestation. When no other inputs are specified, the
|
to generate an SBOM attestation. The `sbom-path` parameter should identify
|
||||||
action will automatically generate an SPDX SBOM by scanning the
|
the SBOM document to be associated with the subject.
|
||||||
`github.workspace` directory.
|
|
||||||
|
|
||||||
### Inputs
|
### Inputs
|
||||||
|
|
||||||
@@ -77,15 +76,6 @@ See [action.yml](action.yml)
|
|||||||
# "scan-path" and "sbom-format" inputs are ignored.
|
# "scan-path" and "sbom-format" inputs are ignored.
|
||||||
sbom-path:
|
sbom-path:
|
||||||
|
|
||||||
# Path on the filesystem to scan for SBOM generation. Ignored if "sbom-path"
|
|
||||||
# is specified. Defaults to ${{ github.workspace }}
|
|
||||||
scan-path:
|
|
||||||
|
|
||||||
# Format to use for the generated SBOM output. Supported formats are
|
|
||||||
# "spdx" and "cyclonedx". Ignored if "sbom-path" is specified. Defaults to
|
|
||||||
# "spdx".
|
|
||||||
sbom-format:
|
|
||||||
|
|
||||||
# Whether to push the attestation to the image registry. Requires that the
|
# Whether to push the attestation to the image registry. Requires that the
|
||||||
# "subject-name" parameter specify the fully-qualified image name and that
|
# "subject-name" parameter specify the fully-qualified image name and that
|
||||||
# the "subject-digest" parameter be specified. Defaults to false.
|
# the "subject-digest" parameter be specified. Defaults to false.
|
||||||
@@ -130,7 +120,8 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
permissions:
|
permissions:
|
||||||
id-token: write
|
id-token: write
|
||||||
contents: write
|
contents: read
|
||||||
|
attestations: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -188,7 +179,8 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
id-token: write
|
id-token: write
|
||||||
packages: write
|
packages: write
|
||||||
contents: write
|
contents: read
|
||||||
|
attestations: write
|
||||||
env:
|
env:
|
||||||
REGISTRY: ghcr.io
|
REGISTRY: ghcr.io
|
||||||
IMAGE_NAME: ${{ github.repository }}
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
@@ -223,7 +215,6 @@ jobs:
|
|||||||
|
|
||||||
[1]: https://github.com/actions/toolkit/tree/main/packages/attest
|
[1]: https://github.com/actions/toolkit/tree/main/packages/attest
|
||||||
[2]: https://github.com/in-toto/attestation/tree/main/spec/v1
|
[2]: https://github.com/in-toto/attestation/tree/main/spec/v1
|
||||||
[3]: https://github.com/anchore/sbom-action
|
|
||||||
[4]: https://spdx.dev/
|
[4]: https://spdx.dev/
|
||||||
[5]: https://cyclonedx.org/
|
[5]: https://cyclonedx.org/
|
||||||
[6]: https://www.sigstore.dev/
|
[6]: https://www.sigstore.dev/
|
||||||
|
|||||||
30
RELEASE.md
Normal file
30
RELEASE.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Release Instructions
|
||||||
|
|
||||||
|
Follow the steps below to tag a new release for the
|
||||||
|
`actions/attest-sbom` action.
|
||||||
|
|
||||||
|
If changes were made to the internal `actions/attest-sbom/predicate`
|
||||||
|
action (any updates to [`./predicate/action.yaml`](./predicate/action.yml) or
|
||||||
|
any of the code in the [`./src`](./src) directory), start with step #1;
|
||||||
|
otherwise, skip directly to step #5.
|
||||||
|
|
||||||
|
1. Merge the latest changes to the `main` branch.
|
||||||
|
1. Create and push a new predicate tag of the form `predicate@X.X.X` following
|
||||||
|
SemVer conventions:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git tag -a "predicate@X.X.X" -m "predicate@X.X.X Release"
|
||||||
|
git push --tags
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Update the reference to the `actions/attest-sbom/predicate`
|
||||||
|
action in [`action.yml`](./action.yml) to point to the SHA of the newly
|
||||||
|
created tag.
|
||||||
|
1. Push the `action.yml` change and open a PR. Once it has been reviewed, merge
|
||||||
|
the PR and proceed with the release instructions.
|
||||||
|
1. Create a new release for the top-level action using a tag of the form
|
||||||
|
`vX.X.X` following SemVer conventions:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
gh release create vX.X.X
|
||||||
|
```
|
||||||
45
action.yml
45
action.yml
@@ -24,18 +24,6 @@ inputs:
|
|||||||
Path to the JSON-formatted SBOM file to attest. When specified, the
|
Path to the JSON-formatted SBOM file to attest. When specified, the
|
||||||
"scan-path" and "sbom-format" inputs are ignored.
|
"scan-path" and "sbom-format" inputs are ignored.
|
||||||
required: false
|
required: false
|
||||||
scan-path:
|
|
||||||
description: >
|
|
||||||
Path on the filesystem to scan for SBOM generation. Ignored if "sbom-path"
|
|
||||||
is specified.
|
|
||||||
default: ${{ github.workspace }}
|
|
||||||
required: false
|
|
||||||
sbom-format:
|
|
||||||
description: >
|
|
||||||
Format to use for the generated SBOM output. Supported formats are "spdx"
|
|
||||||
and "cyclonedx". Ignored if "sbom-path" is specified.
|
|
||||||
default: 'spdx'
|
|
||||||
required: false
|
|
||||||
push-to-registry:
|
push-to-registry:
|
||||||
description: >
|
description: >
|
||||||
Whether to push the provenance statement to the image registry. Requires
|
Whether to push the provenance statement to the image registry. Requires
|
||||||
@@ -48,6 +36,7 @@ inputs:
|
|||||||
The GitHub token used to make authenticated API requests.
|
The GitHub token used to make authenticated API requests.
|
||||||
default: ${{ github.token }}
|
default: ${{ github.token }}
|
||||||
required: false
|
required: false
|
||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
bundle-path:
|
bundle-path:
|
||||||
description: 'The path to the file containing the attestation bundle(s).'
|
description: 'The path to the file containing the attestation bundle(s).'
|
||||||
@@ -56,45 +45,19 @@ outputs:
|
|||||||
runs:
|
runs:
|
||||||
using: 'composite'
|
using: 'composite'
|
||||||
steps:
|
steps:
|
||||||
- name: Generate random SBOM output file name
|
- uses: actions/attest-sbom/predicate@847c6befa7ce187c962fa6c3e6cd3c96e4da9565 # predicate@0.1.0
|
||||||
if: inputs.sbom-path == ''
|
|
||||||
id: sbom-output
|
|
||||||
run:
|
|
||||||
echo "path=${{ runner.temp }}/sbom_$(openssl rand -hex 6).json" >> $GITHUB_OUTPUT
|
|
||||||
shell: bash
|
|
||||||
- name: SBOM format check
|
|
||||||
id: check-sbom-format
|
|
||||||
if: inputs.sbom-path == ''
|
|
||||||
run: |
|
|
||||||
if [ "${{inputs.sbom-format}}" != "spdx" ] && [ "${{inputs.sbom-format}}" != "cyclonedx" ] ]; then
|
|
||||||
echo "Invalid SBOM format. Supported formats are spdx and cyclonedx."
|
|
||||||
exit 1
|
|
||||||
elif [ "${{inputs.sbom-format}}" == "spdx" ]; then
|
|
||||||
echo "format=spdx-json" >> $GITHUB_OUTPUT
|
|
||||||
elif [ "${{inputs.sbom-format}}" == "cyclonedx" ]; then
|
|
||||||
echo "format=cyclonedx-json" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
shell: bash
|
|
||||||
- name: Generate SBOM
|
|
||||||
if: inputs.sbom-path == ''
|
|
||||||
uses: anchore/sbom-action@b6a39da80722a2cb0ef5d197531764a89b5d48c3 # v0.15.8
|
|
||||||
with:
|
|
||||||
path: ${{ inputs.scan-path }}
|
|
||||||
output-file: ${{ steps.sbom-output.outputs.path }}
|
|
||||||
format: ${{ steps.check-sbom-format.outputs.format }}
|
|
||||||
- uses: actions/attest-sbom/predicate@main
|
|
||||||
id: generate-sbom-predicate
|
id: generate-sbom-predicate
|
||||||
with:
|
with:
|
||||||
sbom-path: ${{ inputs.sbom-path || steps.sbom-output.outputs.path }}
|
sbom-path: ${{ inputs.sbom-path || steps.sbom-output.outputs.path }}
|
||||||
- uses: actions/attest@main
|
- uses: actions/attest@14e407ca15f1b08f4869fc058b059f7f1e434df6 # v0.1.0
|
||||||
id: attest
|
id: attest
|
||||||
with:
|
with:
|
||||||
subject-path: ${{ inputs.subject-path }}
|
subject-path: ${{ inputs.subject-path }}
|
||||||
subject-digest: ${{ inputs.subject-digest }}
|
subject-digest: ${{ inputs.subject-digest }}
|
||||||
subject-name: ${{ inputs.subject-name }}
|
subject-name: ${{ inputs.subject-name }}
|
||||||
push-to-registry: ${{ inputs.push-to-registry }}
|
|
||||||
predicate-type:
|
predicate-type:
|
||||||
${{ steps.generate-sbom-predicate.outputs.predicate-type }}
|
${{ steps.generate-sbom-predicate.outputs.predicate-type }}
|
||||||
predicate-path:
|
predicate-path:
|
||||||
${{ steps.generate-sbom-predicate.outputs.predicate-path }}
|
${{ steps.generate-sbom-predicate.outputs.predicate-path }}
|
||||||
|
push-to-registry: ${{ inputs.push-to-registry }}
|
||||||
github-token: ${{ inputs.github-token }}
|
github-token: ${{ inputs.github-token }}
|
||||||
|
|||||||
305
dist/index.js
generated
vendored
305
dist/index.js
generated
vendored
@@ -1590,7 +1590,7 @@ class HttpClient {
|
|||||||
if (this._keepAlive && useProxy) {
|
if (this._keepAlive && useProxy) {
|
||||||
agent = this._proxyAgent;
|
agent = this._proxyAgent;
|
||||||
}
|
}
|
||||||
if (this._keepAlive && !useProxy) {
|
if (!useProxy) {
|
||||||
agent = this._agent;
|
agent = this._agent;
|
||||||
}
|
}
|
||||||
// if agent is already assigned use that agent.
|
// if agent is already assigned use that agent.
|
||||||
@@ -1622,16 +1622,12 @@ class HttpClient {
|
|||||||
agent = tunnelAgent(agentOptions);
|
agent = tunnelAgent(agentOptions);
|
||||||
this._proxyAgent = agent;
|
this._proxyAgent = agent;
|
||||||
}
|
}
|
||||||
// if reusing agent across request and tunneling agent isn't assigned create a new agent
|
// if tunneling agent isn't assigned create a new agent
|
||||||
if (this._keepAlive && !agent) {
|
if (!agent) {
|
||||||
const options = { keepAlive: this._keepAlive, maxSockets };
|
const options = { keepAlive: this._keepAlive, maxSockets };
|
||||||
agent = usingSsl ? new https.Agent(options) : new http.Agent(options);
|
agent = usingSsl ? new https.Agent(options) : new http.Agent(options);
|
||||||
this._agent = agent;
|
this._agent = agent;
|
||||||
}
|
}
|
||||||
// if not using private agent and tunnel agent isn't setup then use global agent
|
|
||||||
if (!agent) {
|
|
||||||
agent = usingSsl ? https.globalAgent : http.globalAgent;
|
|
||||||
}
|
|
||||||
if (usingSsl && this._ignoreSslError) {
|
if (usingSsl && this._ignoreSslError) {
|
||||||
// we don't want to set NODE_TLS_REJECT_UNAUTHORIZED=0 since that will affect request for entire process
|
// we don't want to set NODE_TLS_REJECT_UNAUTHORIZED=0 since that will affect request for entire process
|
||||||
// http.RequestOptions doesn't expose a way to modify RequestOptions.agent.options
|
// http.RequestOptions doesn't expose a way to modify RequestOptions.agent.options
|
||||||
@@ -8446,6 +8442,132 @@ function onConnectTimeout (socket) {
|
|||||||
module.exports = buildConnector
|
module.exports = buildConnector
|
||||||
|
|
||||||
|
|
||||||
|
/***/ }),
|
||||||
|
|
||||||
|
/***/ 4462:
|
||||||
|
/***/ ((module) => {
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
|
||||||
|
/** @type {Record<string, string | undefined>} */
|
||||||
|
const headerNameLowerCasedRecord = {}
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/docs/Web/HTTP/Headers
|
||||||
|
const wellknownHeaderNames = [
|
||||||
|
'Accept',
|
||||||
|
'Accept-Encoding',
|
||||||
|
'Accept-Language',
|
||||||
|
'Accept-Ranges',
|
||||||
|
'Access-Control-Allow-Credentials',
|
||||||
|
'Access-Control-Allow-Headers',
|
||||||
|
'Access-Control-Allow-Methods',
|
||||||
|
'Access-Control-Allow-Origin',
|
||||||
|
'Access-Control-Expose-Headers',
|
||||||
|
'Access-Control-Max-Age',
|
||||||
|
'Access-Control-Request-Headers',
|
||||||
|
'Access-Control-Request-Method',
|
||||||
|
'Age',
|
||||||
|
'Allow',
|
||||||
|
'Alt-Svc',
|
||||||
|
'Alt-Used',
|
||||||
|
'Authorization',
|
||||||
|
'Cache-Control',
|
||||||
|
'Clear-Site-Data',
|
||||||
|
'Connection',
|
||||||
|
'Content-Disposition',
|
||||||
|
'Content-Encoding',
|
||||||
|
'Content-Language',
|
||||||
|
'Content-Length',
|
||||||
|
'Content-Location',
|
||||||
|
'Content-Range',
|
||||||
|
'Content-Security-Policy',
|
||||||
|
'Content-Security-Policy-Report-Only',
|
||||||
|
'Content-Type',
|
||||||
|
'Cookie',
|
||||||
|
'Cross-Origin-Embedder-Policy',
|
||||||
|
'Cross-Origin-Opener-Policy',
|
||||||
|
'Cross-Origin-Resource-Policy',
|
||||||
|
'Date',
|
||||||
|
'Device-Memory',
|
||||||
|
'Downlink',
|
||||||
|
'ECT',
|
||||||
|
'ETag',
|
||||||
|
'Expect',
|
||||||
|
'Expect-CT',
|
||||||
|
'Expires',
|
||||||
|
'Forwarded',
|
||||||
|
'From',
|
||||||
|
'Host',
|
||||||
|
'If-Match',
|
||||||
|
'If-Modified-Since',
|
||||||
|
'If-None-Match',
|
||||||
|
'If-Range',
|
||||||
|
'If-Unmodified-Since',
|
||||||
|
'Keep-Alive',
|
||||||
|
'Last-Modified',
|
||||||
|
'Link',
|
||||||
|
'Location',
|
||||||
|
'Max-Forwards',
|
||||||
|
'Origin',
|
||||||
|
'Permissions-Policy',
|
||||||
|
'Pragma',
|
||||||
|
'Proxy-Authenticate',
|
||||||
|
'Proxy-Authorization',
|
||||||
|
'RTT',
|
||||||
|
'Range',
|
||||||
|
'Referer',
|
||||||
|
'Referrer-Policy',
|
||||||
|
'Refresh',
|
||||||
|
'Retry-After',
|
||||||
|
'Sec-WebSocket-Accept',
|
||||||
|
'Sec-WebSocket-Extensions',
|
||||||
|
'Sec-WebSocket-Key',
|
||||||
|
'Sec-WebSocket-Protocol',
|
||||||
|
'Sec-WebSocket-Version',
|
||||||
|
'Server',
|
||||||
|
'Server-Timing',
|
||||||
|
'Service-Worker-Allowed',
|
||||||
|
'Service-Worker-Navigation-Preload',
|
||||||
|
'Set-Cookie',
|
||||||
|
'SourceMap',
|
||||||
|
'Strict-Transport-Security',
|
||||||
|
'Supports-Loading-Mode',
|
||||||
|
'TE',
|
||||||
|
'Timing-Allow-Origin',
|
||||||
|
'Trailer',
|
||||||
|
'Transfer-Encoding',
|
||||||
|
'Upgrade',
|
||||||
|
'Upgrade-Insecure-Requests',
|
||||||
|
'User-Agent',
|
||||||
|
'Vary',
|
||||||
|
'Via',
|
||||||
|
'WWW-Authenticate',
|
||||||
|
'X-Content-Type-Options',
|
||||||
|
'X-DNS-Prefetch-Control',
|
||||||
|
'X-Frame-Options',
|
||||||
|
'X-Permitted-Cross-Domain-Policies',
|
||||||
|
'X-Powered-By',
|
||||||
|
'X-Requested-With',
|
||||||
|
'X-XSS-Protection'
|
||||||
|
]
|
||||||
|
|
||||||
|
for (let i = 0; i < wellknownHeaderNames.length; ++i) {
|
||||||
|
const key = wellknownHeaderNames[i]
|
||||||
|
const lowerCasedKey = key.toLowerCase()
|
||||||
|
headerNameLowerCasedRecord[key] = headerNameLowerCasedRecord[lowerCasedKey] =
|
||||||
|
lowerCasedKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`.
|
||||||
|
Object.setPrototypeOf(headerNameLowerCasedRecord, null)
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
wellknownHeaderNames,
|
||||||
|
headerNameLowerCasedRecord
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/***/ }),
|
/***/ }),
|
||||||
|
|
||||||
/***/ 8045:
|
/***/ 8045:
|
||||||
@@ -9278,6 +9400,7 @@ const { InvalidArgumentError } = __nccwpck_require__(8045)
|
|||||||
const { Blob } = __nccwpck_require__(4300)
|
const { Blob } = __nccwpck_require__(4300)
|
||||||
const nodeUtil = __nccwpck_require__(3837)
|
const nodeUtil = __nccwpck_require__(3837)
|
||||||
const { stringify } = __nccwpck_require__(3477)
|
const { stringify } = __nccwpck_require__(3477)
|
||||||
|
const { headerNameLowerCasedRecord } = __nccwpck_require__(4462)
|
||||||
|
|
||||||
const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(v => Number(v))
|
const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(v => Number(v))
|
||||||
|
|
||||||
@@ -9487,6 +9610,15 @@ function parseKeepAliveTimeout (val) {
|
|||||||
return m ? parseInt(m[1], 10) * 1000 : null
|
return m ? parseInt(m[1], 10) * 1000 : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a header name and returns its lowercase value.
|
||||||
|
* @param {string | Buffer} value Header name
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function headerNameToString (value) {
|
||||||
|
return headerNameLowerCasedRecord[value] || value.toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
function parseHeaders (headers, obj = {}) {
|
function parseHeaders (headers, obj = {}) {
|
||||||
// For H2 support
|
// For H2 support
|
||||||
if (!Array.isArray(headers)) return headers
|
if (!Array.isArray(headers)) return headers
|
||||||
@@ -9758,6 +9890,7 @@ module.exports = {
|
|||||||
isIterable,
|
isIterable,
|
||||||
isAsyncIterable,
|
isAsyncIterable,
|
||||||
isDestroyed,
|
isDestroyed,
|
||||||
|
headerNameToString,
|
||||||
parseRawHeaders,
|
parseRawHeaders,
|
||||||
parseHeaders,
|
parseHeaders,
|
||||||
parseKeepAliveTimeout,
|
parseKeepAliveTimeout,
|
||||||
@@ -16405,14 +16538,18 @@ const { isBlobLike, toUSVString, ReadableStreamFrom } = __nccwpck_require__(3983
|
|||||||
const assert = __nccwpck_require__(9491)
|
const assert = __nccwpck_require__(9491)
|
||||||
const { isUint8Array } = __nccwpck_require__(9830)
|
const { isUint8Array } = __nccwpck_require__(9830)
|
||||||
|
|
||||||
|
let supportedHashes = []
|
||||||
|
|
||||||
// https://nodejs.org/api/crypto.html#determining-if-crypto-support-is-unavailable
|
// https://nodejs.org/api/crypto.html#determining-if-crypto-support-is-unavailable
|
||||||
/** @type {import('crypto')|undefined} */
|
/** @type {import('crypto')|undefined} */
|
||||||
let crypto
|
let crypto
|
||||||
|
|
||||||
try {
|
try {
|
||||||
crypto = __nccwpck_require__(6113)
|
crypto = __nccwpck_require__(6113)
|
||||||
|
const possibleRelevantHashes = ['sha256', 'sha384', 'sha512']
|
||||||
|
supportedHashes = crypto.getHashes().filter((hash) => possibleRelevantHashes.includes(hash))
|
||||||
|
/* c8 ignore next 3 */
|
||||||
} catch {
|
} catch {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function responseURL (response) {
|
function responseURL (response) {
|
||||||
@@ -16940,66 +17077,56 @@ function bytesMatch (bytes, metadataList) {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. If parsedMetadata is the empty set, return true.
|
// 3. If response is not eligible for integrity validation, return false.
|
||||||
|
// TODO
|
||||||
|
|
||||||
|
// 4. If parsedMetadata is the empty set, return true.
|
||||||
if (parsedMetadata.length === 0) {
|
if (parsedMetadata.length === 0) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Let metadata be the result of getting the strongest
|
// 5. Let metadata be the result of getting the strongest
|
||||||
// metadata from parsedMetadata.
|
// metadata from parsedMetadata.
|
||||||
const list = parsedMetadata.sort((c, d) => d.algo.localeCompare(c.algo))
|
const strongest = getStrongestMetadata(parsedMetadata)
|
||||||
// get the strongest algorithm
|
const metadata = filterMetadataListByAlgorithm(parsedMetadata, strongest)
|
||||||
const strongest = list[0].algo
|
|
||||||
// get all entries that use the strongest algorithm; ignore weaker
|
|
||||||
const metadata = list.filter((item) => item.algo === strongest)
|
|
||||||
|
|
||||||
// 5. For each item in metadata:
|
// 6. For each item in metadata:
|
||||||
for (const item of metadata) {
|
for (const item of metadata) {
|
||||||
// 1. Let algorithm be the alg component of item.
|
// 1. Let algorithm be the alg component of item.
|
||||||
const algorithm = item.algo
|
const algorithm = item.algo
|
||||||
|
|
||||||
// 2. Let expectedValue be the val component of item.
|
// 2. Let expectedValue be the val component of item.
|
||||||
let expectedValue = item.hash
|
const expectedValue = item.hash
|
||||||
|
|
||||||
// See https://github.com/web-platform-tests/wpt/commit/e4c5cc7a5e48093220528dfdd1c4012dc3837a0e
|
// See https://github.com/web-platform-tests/wpt/commit/e4c5cc7a5e48093220528dfdd1c4012dc3837a0e
|
||||||
// "be liberal with padding". This is annoying, and it's not even in the spec.
|
// "be liberal with padding". This is annoying, and it's not even in the spec.
|
||||||
|
|
||||||
if (expectedValue.endsWith('==')) {
|
|
||||||
expectedValue = expectedValue.slice(0, -2)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Let actualValue be the result of applying algorithm to bytes.
|
// 3. Let actualValue be the result of applying algorithm to bytes.
|
||||||
let actualValue = crypto.createHash(algorithm).update(bytes).digest('base64')
|
let actualValue = crypto.createHash(algorithm).update(bytes).digest('base64')
|
||||||
|
|
||||||
if (actualValue.endsWith('==')) {
|
if (actualValue[actualValue.length - 1] === '=') {
|
||||||
|
if (actualValue[actualValue.length - 2] === '=') {
|
||||||
actualValue = actualValue.slice(0, -2)
|
actualValue = actualValue.slice(0, -2)
|
||||||
|
} else {
|
||||||
|
actualValue = actualValue.slice(0, -1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. If actualValue is a case-sensitive match for expectedValue,
|
// 4. If actualValue is a case-sensitive match for expectedValue,
|
||||||
// return true.
|
// return true.
|
||||||
if (actualValue === expectedValue) {
|
if (compareBase64Mixed(actualValue, expectedValue)) {
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
let actualBase64URL = crypto.createHash(algorithm).update(bytes).digest('base64url')
|
|
||||||
|
|
||||||
if (actualBase64URL.endsWith('==')) {
|
|
||||||
actualBase64URL = actualBase64URL.slice(0, -2)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (actualBase64URL === expectedValue) {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Return false.
|
// 7. Return false.
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://w3c.github.io/webappsec-subresource-integrity/#grammardef-hash-with-options
|
// https://w3c.github.io/webappsec-subresource-integrity/#grammardef-hash-with-options
|
||||||
// https://www.w3.org/TR/CSP2/#source-list-syntax
|
// https://www.w3.org/TR/CSP2/#source-list-syntax
|
||||||
// https://www.rfc-editor.org/rfc/rfc5234#appendix-B.1
|
// https://www.rfc-editor.org/rfc/rfc5234#appendix-B.1
|
||||||
const parseHashWithOptions = /((?<algo>sha256|sha384|sha512)-(?<hash>[A-z0-9+/]{1}.*={0,2}))( +[\x21-\x7e]?)?/i
|
const parseHashWithOptions = /(?<algo>sha256|sha384|sha512)-((?<hash>[A-Za-z0-9+/]+|[A-Za-z0-9_-]+)={0,2}(?:\s|$)( +[!-~]*)?)?/i
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata
|
* @see https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata
|
||||||
@@ -17013,8 +17140,6 @@ function parseMetadata (metadata) {
|
|||||||
// 2. Let empty be equal to true.
|
// 2. Let empty be equal to true.
|
||||||
let empty = true
|
let empty = true
|
||||||
|
|
||||||
const supportedHashes = crypto.getHashes()
|
|
||||||
|
|
||||||
// 3. For each token returned by splitting metadata on spaces:
|
// 3. For each token returned by splitting metadata on spaces:
|
||||||
for (const token of metadata.split(' ')) {
|
for (const token of metadata.split(' ')) {
|
||||||
// 1. Set empty to false.
|
// 1. Set empty to false.
|
||||||
@@ -17024,7 +17149,11 @@ function parseMetadata (metadata) {
|
|||||||
const parsedToken = parseHashWithOptions.exec(token)
|
const parsedToken = parseHashWithOptions.exec(token)
|
||||||
|
|
||||||
// 3. If token does not parse, continue to the next token.
|
// 3. If token does not parse, continue to the next token.
|
||||||
if (parsedToken === null || parsedToken.groups === undefined) {
|
if (
|
||||||
|
parsedToken === null ||
|
||||||
|
parsedToken.groups === undefined ||
|
||||||
|
parsedToken.groups.algo === undefined
|
||||||
|
) {
|
||||||
// Note: Chromium blocks the request at this point, but Firefox
|
// Note: Chromium blocks the request at this point, but Firefox
|
||||||
// gives a warning that an invalid integrity was given. The
|
// gives a warning that an invalid integrity was given. The
|
||||||
// correct behavior is to ignore these, and subsequently not
|
// correct behavior is to ignore these, and subsequently not
|
||||||
@@ -17033,11 +17162,11 @@ function parseMetadata (metadata) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 4. Let algorithm be the hash-algo component of token.
|
// 4. Let algorithm be the hash-algo component of token.
|
||||||
const algorithm = parsedToken.groups.algo
|
const algorithm = parsedToken.groups.algo.toLowerCase()
|
||||||
|
|
||||||
// 5. If algorithm is a hash function recognized by the user
|
// 5. If algorithm is a hash function recognized by the user
|
||||||
// agent, add the parsed token to result.
|
// agent, add the parsed token to result.
|
||||||
if (supportedHashes.includes(algorithm.toLowerCase())) {
|
if (supportedHashes.includes(algorithm)) {
|
||||||
result.push(parsedToken.groups)
|
result.push(parsedToken.groups)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17050,6 +17179,82 @@ function parseMetadata (metadata) {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {{ algo: 'sha256' | 'sha384' | 'sha512' }[]} metadataList
|
||||||
|
*/
|
||||||
|
function getStrongestMetadata (metadataList) {
|
||||||
|
// Let algorithm be the algo component of the first item in metadataList.
|
||||||
|
// Can be sha256
|
||||||
|
let algorithm = metadataList[0].algo
|
||||||
|
// If the algorithm is sha512, then it is the strongest
|
||||||
|
// and we can return immediately
|
||||||
|
if (algorithm[3] === '5') {
|
||||||
|
return algorithm
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 1; i < metadataList.length; ++i) {
|
||||||
|
const metadata = metadataList[i]
|
||||||
|
// If the algorithm is sha512, then it is the strongest
|
||||||
|
// and we can break the loop immediately
|
||||||
|
if (metadata.algo[3] === '5') {
|
||||||
|
algorithm = 'sha512'
|
||||||
|
break
|
||||||
|
// If the algorithm is sha384, then a potential sha256 or sha384 is ignored
|
||||||
|
} else if (algorithm[3] === '3') {
|
||||||
|
continue
|
||||||
|
// algorithm is sha256, check if algorithm is sha384 and if so, set it as
|
||||||
|
// the strongest
|
||||||
|
} else if (metadata.algo[3] === '3') {
|
||||||
|
algorithm = 'sha384'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return algorithm
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterMetadataListByAlgorithm (metadataList, algorithm) {
|
||||||
|
if (metadataList.length === 1) {
|
||||||
|
return metadataList
|
||||||
|
}
|
||||||
|
|
||||||
|
let pos = 0
|
||||||
|
for (let i = 0; i < metadataList.length; ++i) {
|
||||||
|
if (metadataList[i].algo === algorithm) {
|
||||||
|
metadataList[pos++] = metadataList[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
metadataList.length = pos
|
||||||
|
|
||||||
|
return metadataList
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares two base64 strings, allowing for base64url
|
||||||
|
* in the second string.
|
||||||
|
*
|
||||||
|
* @param {string} actualValue always base64
|
||||||
|
* @param {string} expectedValue base64 or base64url
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function compareBase64Mixed (actualValue, expectedValue) {
|
||||||
|
if (actualValue.length !== expectedValue.length) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for (let i = 0; i < actualValue.length; ++i) {
|
||||||
|
if (actualValue[i] !== expectedValue[i]) {
|
||||||
|
if (
|
||||||
|
(actualValue[i] === '+' && expectedValue[i] === '-') ||
|
||||||
|
(actualValue[i] === '/' && expectedValue[i] === '_')
|
||||||
|
) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// https://w3c.github.io/webappsec-upgrade-insecure-requests/#upgrade-request
|
// https://w3c.github.io/webappsec-upgrade-insecure-requests/#upgrade-request
|
||||||
function tryUpgradeRequestToAPotentiallyTrustworthyURL (request) {
|
function tryUpgradeRequestToAPotentiallyTrustworthyURL (request) {
|
||||||
// TODO
|
// TODO
|
||||||
@@ -17465,7 +17670,8 @@ module.exports = {
|
|||||||
urlHasHttpsScheme,
|
urlHasHttpsScheme,
|
||||||
urlIsHttpHttpsScheme,
|
urlIsHttpHttpsScheme,
|
||||||
readAllBytes,
|
readAllBytes,
|
||||||
normalizeMethodRecord
|
normalizeMethodRecord,
|
||||||
|
parseMetadata
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -19552,12 +19758,17 @@ function parseLocation (statusCode, headers) {
|
|||||||
|
|
||||||
// https://tools.ietf.org/html/rfc7231#section-6.4.4
|
// https://tools.ietf.org/html/rfc7231#section-6.4.4
|
||||||
function shouldRemoveHeader (header, removeContent, unknownOrigin) {
|
function shouldRemoveHeader (header, removeContent, unknownOrigin) {
|
||||||
return (
|
if (header.length === 4) {
|
||||||
(header.length === 4 && header.toString().toLowerCase() === 'host') ||
|
return util.headerNameToString(header) === 'host'
|
||||||
(removeContent && header.toString().toLowerCase().indexOf('content-') === 0) ||
|
}
|
||||||
(unknownOrigin && header.length === 13 && header.toString().toLowerCase() === 'authorization') ||
|
if (removeContent && util.headerNameToString(header).startsWith('content-')) {
|
||||||
(unknownOrigin && header.length === 6 && header.toString().toLowerCase() === 'cookie')
|
return true
|
||||||
)
|
}
|
||||||
|
if (unknownOrigin && (header.length === 13 || header.length === 6 || header.length === 19)) {
|
||||||
|
const name = util.headerNameToString(header)
|
||||||
|
return name === 'authorization' || name === 'cookie' || name === 'proxy-authorization'
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://tools.ietf.org/html/rfc7231#section-6.4
|
// https://tools.ietf.org/html/rfc7231#section-6.4
|
||||||
|
|||||||
2104
package-lock.json
generated
2104
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
30
package.json
30
package.json
@@ -1,21 +1,21 @@
|
|||||||
{
|
{
|
||||||
"name": "typescript-action",
|
"name": "actions/attest-sbom",
|
||||||
"description": "GitHub Actions TypeScript template",
|
"description": "Generate signed SBOM attestations",
|
||||||
"version": "0.0.0",
|
"version": "1.0.0",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
"homepage": "https://github.com/actions/typescript-action",
|
"homepage": "https://github.com/actions/attest-sbom",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/actions/typescript-action.git"
|
"url": "git+https://github.com/actions/attest-sbom.git"
|
||||||
},
|
},
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/actions/typescript-action/issues"
|
"url": "https://github.com/actions/attest-sbom/issues"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"actions",
|
"actions",
|
||||||
"node",
|
"attestation",
|
||||||
"setup"
|
"sbom"
|
||||||
],
|
],
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./dist/index.js"
|
".": "./dist/index.js"
|
||||||
@@ -73,22 +73,22 @@
|
|||||||
"@actions/core": "^1.10.1"
|
"@actions/core": "^1.10.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@actions/attest": "^1.0.0",
|
"@actions/attest": "^1.1.0",
|
||||||
"@types/jest": "^29.5.12",
|
"@types/jest": "^29.5.12",
|
||||||
"@types/node": "^20.11.24",
|
"@types/node": "^20.12.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
"@typescript-eslint/eslint-plugin": "^7.7.0",
|
||||||
"@typescript-eslint/parser": "^6.21.0",
|
"@typescript-eslint/parser": "^7.7.0",
|
||||||
"@vercel/ncc": "^0.38.1",
|
"@vercel/ncc": "^0.38.1",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-plugin-github": "^4.10.2",
|
"eslint-plugin-github": "^4.10.2",
|
||||||
"eslint-plugin-jest": "^27.9.0",
|
"eslint-plugin-jest": "^28.2.0",
|
||||||
"eslint-plugin-jsonc": "^2.13.0",
|
"eslint-plugin-jsonc": "^2.15.1",
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"markdownlint-cli": "^0.39.0",
|
"markdownlint-cli": "^0.39.0",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"prettier-eslint": "^16.3.0",
|
"prettier-eslint": "^16.3.0",
|
||||||
"ts-jest": "^29.1.2",
|
"ts-jest": "^29.1.2",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.4.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# About:
|
|
||||||
#
|
|
||||||
# This is a helper script to tag and push a new release. GitHub Actions use
|
|
||||||
# release tags to allow users to select a specific version of the action to use.
|
|
||||||
#
|
|
||||||
# See: https://github.com/actions/typescript-action#publishing-a-new-release
|
|
||||||
#
|
|
||||||
# This script will do the following:
|
|
||||||
#
|
|
||||||
# 1. Get the latest release tag
|
|
||||||
# 2. Prompt the user for a new release tag
|
|
||||||
# 3. Tag the new release
|
|
||||||
# 4. Push the new tag to the remote
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
#
|
|
||||||
# script/release
|
|
||||||
|
|
||||||
# Terminal colors
|
|
||||||
OFF='\033[0m'
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
|
|
||||||
# Get the latest release tag
|
|
||||||
latest_tag=$(git describe --tags "$(git rev-list --tags --max-count=1)")
|
|
||||||
|
|
||||||
if [[ -z "$latest_tag" ]]; then
|
|
||||||
# There are no existing release tags
|
|
||||||
echo -e "No tags found (yet) - Continue to create and push your first tag"
|
|
||||||
latest_tag="[unknown]"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Display the latest release tag
|
|
||||||
echo -e "The latest release tag is: ${BLUE}${latest_tag}${OFF}"
|
|
||||||
|
|
||||||
# Prompt the user for the new release tag
|
|
||||||
read -r -p 'Enter a new release tag (vX.X.X format): ' new_tag
|
|
||||||
|
|
||||||
# Validate the new release tag
|
|
||||||
tag_regex='v[0-9]+\.[0-9]+\.[0-9]+$'
|
|
||||||
if echo "$new_tag" | grep -q -E "$tag_regex"; then
|
|
||||||
echo -e "Tag: ${BLUE}$new_tag${OFF} is valid"
|
|
||||||
else
|
|
||||||
# Release tag is not `vX.X.X` format
|
|
||||||
echo -e "Tag: ${BLUE}$new_tag${OFF} is ${RED}not valid${OFF} (must be in vX.X.X format)"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Tag the new release
|
|
||||||
git tag -a "$new_tag" -m "$new_tag Release"
|
|
||||||
echo -e "${GREEN}Tagged: $new_tag${OFF}"
|
|
||||||
|
|
||||||
# Push the new tag to the remote
|
|
||||||
git push --tags
|
|
||||||
echo -e "${GREEN}Release tag pushed to remote${OFF}"
|
|
||||||
echo -e "${GREEN}Done!${OFF}"
|
|
||||||
Reference in New Issue
Block a user