mirror of https://github.com/actions/toolkit
Merge remote-tracking branch 'actions/main' into add-archive-extractor
commit
5c16ddf520
|
@ -26,15 +26,10 @@ jobs:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set Node.js 16.x
|
- name: Set Node.js 20.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 16.x
|
node-version: 20.x
|
||||||
|
|
||||||
# In order to upload & download artifacts from a shell script, certain env variables need to be set that are only available in the
|
|
||||||
# node context. This runs a local action that gets and sets the necessary env variables that are needed
|
|
||||||
- name: Set env variables
|
|
||||||
uses: ./packages/artifact/__tests__/ci-test-action/
|
|
||||||
|
|
||||||
# Need root node_modules because certain npm packages like jest are configured for the entire repository and it won't be possible
|
# Need root node_modules because certain npm packages like jest are configured for the entire repository and it won't be possible
|
||||||
# without these to just compile the artifacts package
|
# without these to just compile the artifacts package
|
||||||
|
@ -50,48 +45,91 @@ jobs:
|
||||||
- name: Set artifact file contents
|
- name: Set artifact file contents
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
echo "non-gzip-artifact-content=hello" >> $GITHUB_ENV
|
echo "file1=hello from file 1" >> $GITHUB_ENV
|
||||||
echo "gzip-artifact-content=Some large amount of text that has a compression ratio that is greater than 100%. If greater than 100%, gzip is used to upload the file" >> $GITHUB_ENV
|
echo "file2=hello from file 2" >> $GITHUB_ENV
|
||||||
echo "empty-artifact-content=_EMPTY_" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Create files that will be uploaded
|
- name: Create files that will be uploaded
|
||||||
run: |
|
run: |
|
||||||
mkdir artifact-path
|
mkdir artifact-path
|
||||||
echo '${{ env.non-gzip-artifact-content }}' > artifact-path/world.txt
|
echo '${{ env.file1 }}' > artifact-path/first.txt
|
||||||
echo '${{ env.gzip-artifact-content }}' > artifact-path/gzip.txt
|
echo '${{ env.file2 }}' > artifact-path/second.txt
|
||||||
touch artifact-path/empty.txt
|
|
||||||
|
|
||||||
# We're using node -e to call the functions directly available in the @actions/artifact package
|
- name: Upload Artifacts using actions/github-script@v6
|
||||||
- name: Upload artifacts using uploadArtifact()
|
uses: actions/github-script@v6
|
||||||
run: |
|
with:
|
||||||
node -e "Promise.resolve(require('./packages/artifact/lib/artifact-client').create().uploadArtifact('my-artifact-1',['artifact-path/world.txt'], process.argv[1]))" "${{ github.workspace }}"
|
script: |
|
||||||
node -e "Promise.resolve(require('./packages/artifact/lib/artifact-client').create().uploadArtifact('my-artifact-2',['artifact-path/gzip.txt'], process.argv[1]))" "${{ github.workspace }}"
|
const artifact = require('./packages/artifact/lib/artifact')
|
||||||
node -e "Promise.resolve(require('./packages/artifact/lib/artifact-client').create().uploadArtifact('my-artifact-3',['artifact-path/empty.txt'], process.argv[1]))" "${{ github.workspace }}"
|
|
||||||
|
|
||||||
- name: Download artifacts using downloadArtifact()
|
const artifactName = 'my-artifact-${{ matrix.runs-on }}'
|
||||||
run: |
|
console.log('artifactName: ' + artifactName)
|
||||||
mkdir artifact-1-directory
|
|
||||||
node -e "Promise.resolve(require('./packages/artifact/lib/artifact-client').create().downloadArtifact('my-artifact-1','artifact-1-directory'))"
|
|
||||||
mkdir artifact-2-directory
|
|
||||||
node -e "Promise.resolve(require('./packages/artifact/lib/artifact-client').create().downloadArtifact('my-artifact-2','artifact-2-directory'))"
|
|
||||||
mkdir artifact-3-directory
|
|
||||||
node -e "Promise.resolve(require('./packages/artifact/lib/artifact-client').create().downloadArtifact('my-artifact-3','artifact-3-directory'))"
|
|
||||||
|
|
||||||
- name: Verify downloadArtifact()
|
const fileContents = ['artifact-path/first.txt','artifact-path/second.txt']
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
packages/artifact/__tests__/test-artifact-file.sh "artifact-1-directory/artifact-path/world.txt" "${{ env.non-gzip-artifact-content }}"
|
|
||||||
packages/artifact/__tests__/test-artifact-file.sh "artifact-2-directory/artifact-path/gzip.txt" "${{ env.gzip-artifact-content }}"
|
|
||||||
packages/artifact/__tests__/test-artifact-file.sh "artifact-3-directory/artifact-path/empty.txt" "${{ env.empty-artifact-content }}"
|
|
||||||
|
|
||||||
- name: Download artifacts using downloadAllArtifacts()
|
const uploadResult = await artifact.create().uploadArtifact(artifactName, fileContents, './')
|
||||||
run: |
|
console.log(uploadResult)
|
||||||
mkdir multi-artifact-directory
|
|
||||||
node -e "Promise.resolve(require('./packages/artifact/lib/artifact-client').create().downloadAllArtifacts('multi-artifact-directory'))"
|
|
||||||
|
|
||||||
- name: Verify downloadAllArtifacts()
|
const success = uploadResult.success
|
||||||
shell: bash
|
const size = uploadResult.size
|
||||||
|
const id = uploadResult.id
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
throw new Error('Failed to upload artifact')
|
||||||
|
} else {
|
||||||
|
console.log(`Successfully uploaded artifact ${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
verify:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [build]
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set Node.js 20.x
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 20.x
|
||||||
|
|
||||||
|
# Need root node_modules because certain npm packages like jest are configured for the entire repository and it won't be possible
|
||||||
|
# without these to just compile the artifacts package
|
||||||
|
- name: Install root npm packages
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Compile artifact package
|
||||||
run: |
|
run: |
|
||||||
packages/artifact/__tests__/test-artifact-file.sh "multi-artifact-directory/my-artifact-1/artifact-path/world.txt" "${{ env.non-gzip-artifact-content }}"
|
npm ci
|
||||||
packages/artifact/__tests__/test-artifact-file.sh "multi-artifact-directory/my-artifact-2/artifact-path/gzip.txt" "${{ env.gzip-artifact-content }}"
|
npm run tsc
|
||||||
packages/artifact/__tests__/test-artifact-file.sh "multi-artifact-directory/my-artifact-3/artifact-path/empty.txt" "${{ env.empty-artifact-content }}"
|
working-directory: packages/artifact
|
||||||
|
|
||||||
|
- name: List artifacts using actions/github-script@v6
|
||||||
|
uses: actions/github-script@v6
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const artifact = require('./packages/artifact/lib/artifact')
|
||||||
|
|
||||||
|
const workflowRunId = process.env.GITHUB_RUN_ID
|
||||||
|
const repository = process.env.GITHUB_REPOSITORY
|
||||||
|
const repositoryOwner = repository.split('/')[0]
|
||||||
|
const repositoryName = repository.split('/')[1]
|
||||||
|
|
||||||
|
const listResult = await artifact.create().listArtifacts(workflowRunId, repositoryOwner, repositoryName, '${{ secrets.GITHUB_TOKEN }}')
|
||||||
|
console.log(listResult)
|
||||||
|
|
||||||
|
const artifacts = listResult.artifacts
|
||||||
|
|
||||||
|
if (artifacts.length !== 3) {
|
||||||
|
throw new Error('Expected 3 artifacts but only found ' + artifacts.length + ' artifacts')
|
||||||
|
}
|
||||||
|
|
||||||
|
const artifactNames = artifacts.map(artifact => artifact.name)
|
||||||
|
if (!artifactNames.includes('my-artifact-ubuntu-latest')){
|
||||||
|
throw new Error("Expected artifact list to contain an artifact named my-artifact-ubuntu-latest but it's missing")
|
||||||
|
}
|
||||||
|
if (!artifactNames.includes('my-artifact-windows-latest')){
|
||||||
|
throw new Error("Expected artifact list to contain an artifact named my-artifact-windows-latest but it's missing")
|
||||||
|
}
|
||||||
|
if (!artifactNames.includes('my-artifact-macos-latest')){
|
||||||
|
throw new Error("Expected artifact list to contain an artifact named my-artifact-macos-latest but it's missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Successfully listed artifacts that were uploaded')
|
||||||
|
|
|
@ -20,10 +20,10 @@ jobs:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set Node.js 16.x
|
- name: Set Node.js 20.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 16.x
|
node-version: 20.x
|
||||||
|
|
||||||
- name: npm install
|
- name: npm install
|
||||||
run: npm install
|
run: npm install
|
||||||
|
|
|
@ -24,10 +24,10 @@ jobs:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set Node.js 16.x
|
- name: Set Node.js 20.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 16.x
|
node-version: 20.x
|
||||||
|
|
||||||
# In order to save & restore cache from a shell script, certain env variables need to be set that are only available in the
|
# In order to save & restore cache from a shell script, certain env variables need to be set that are only available in the
|
||||||
# node context. This runs a local action that gets and sets the necessary env variables that are needed
|
# node context. This runs a local action that gets and sets the necessary env variables that are needed
|
||||||
|
|
|
@ -23,10 +23,10 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
rm "C:\Program Files\Git\usr\bin\tar.exe"
|
rm "C:\Program Files\Git\usr\bin\tar.exe"
|
||||||
|
|
||||||
- name: Set Node.js 16.x
|
- name: Set Node.js 20.x
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 16.x
|
node-version: 20.x
|
||||||
|
|
||||||
# In order to save & restore cache from a shell script, certain env variables need to be set that are only available in the
|
# In order to save & restore cache from a shell script, certain env variables need to be set that are only available in the
|
||||||
# node context. This runs a local action that gets and sets the necessary env variables that are needed
|
# node context. This runs a local action that gets and sets the necessary env variables that are needed
|
||||||
|
|
|
@ -24,14 +24,14 @@ jobs:
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v1
|
uses: github/codeql-action/init@v2
|
||||||
with:
|
with:
|
||||||
languages: javascript
|
languages: javascript
|
||||||
|
|
||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v1
|
uses: github/codeql-action/autobuild@v2
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v1
|
uses: github/codeql-action/analyze@v2
|
||||||
|
|
|
@ -18,10 +18,10 @@ jobs:
|
||||||
- name: verify package exists
|
- name: verify package exists
|
||||||
run: ls packages/${{ github.event.inputs.package }}
|
run: ls packages/${{ github.event.inputs.package }}
|
||||||
|
|
||||||
- name: Set Node.js 16.x
|
- name: Set Node.js 20.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 16.x
|
node-version: 20.x
|
||||||
|
|
||||||
- name: npm install
|
- name: npm install
|
||||||
run: npm install
|
run: npm install
|
||||||
|
|
|
@ -25,10 +25,10 @@ jobs:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set Node.js 16.x
|
- name: Set Node.js 20.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 16.x
|
node-version: 20.x
|
||||||
|
|
||||||
- name: npm install
|
- name: npm install
|
||||||
run: npm install
|
run: npm install
|
||||||
|
@ -40,7 +40,7 @@ jobs:
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
- name: npm test
|
- name: npm test
|
||||||
run: npm test -- --runInBand
|
run: npm test -- --runInBand --forceExit
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
|
@ -16,8 +16,8 @@
|
||||||
"test": "jest --testTimeout 10000"
|
"test": "jest --testTimeout 10000"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^27.0.2",
|
"@types/jest": "^29.5.4",
|
||||||
"@types/node": "^16.18.1",
|
"@types/node": "^20.5.7",
|
||||||
"@types/signale": "^1.4.1",
|
"@types/signale": "^1.4.1",
|
||||||
"concurrently": "^6.1.0",
|
"concurrently": "^6.1.0",
|
||||||
"eslint": "^8.0.1",
|
"eslint": "^8.0.1",
|
||||||
|
@ -26,11 +26,11 @@
|
||||||
"eslint-plugin-jest": "^27.2.3",
|
"eslint-plugin-jest": "^27.2.3",
|
||||||
"eslint-plugin-prettier": "^5.0.0",
|
"eslint-plugin-prettier": "^5.0.0",
|
||||||
"flow-bin": "^0.115.0",
|
"flow-bin": "^0.115.0",
|
||||||
"jest": "^27.2.5",
|
"jest": "^29.6.4",
|
||||||
"lerna": "^7.1.4",
|
"lerna": "^7.1.4",
|
||||||
"nx": "16.6.0",
|
"nx": "16.6.0",
|
||||||
"prettier": "^3.0.0",
|
"prettier": "^3.0.0",
|
||||||
"ts-jest": "^27.0.5",
|
"ts-jest": "^29.1.1",
|
||||||
"typescript": "^3.9.9"
|
"typescript": "^5.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,279 @@
|
||||||
|
import fs from 'fs'
|
||||||
|
import * as http from 'http'
|
||||||
|
import * as net from 'net'
|
||||||
|
import * as path from 'path'
|
||||||
|
import * as core from '@actions/core'
|
||||||
|
import * as github from '@actions/github'
|
||||||
|
import {HttpClient} from '@actions/http-client'
|
||||||
|
import type {RestEndpointMethods} from '@octokit/plugin-rest-endpoint-methods/dist-types/generated/method-types'
|
||||||
|
import archiver from 'archiver'
|
||||||
|
|
||||||
|
import {downloadArtifact} from '../src/internal/download/download-artifact'
|
||||||
|
import {getUserAgentString} from '../src/internal/shared/user-agent'
|
||||||
|
|
||||||
|
type MockedDownloadArtifact = jest.MockedFunction<
|
||||||
|
RestEndpointMethods['actions']['downloadArtifact']
|
||||||
|
>
|
||||||
|
|
||||||
|
const testDir = path.join(__dirname, '_temp', 'download-artifact')
|
||||||
|
const fixtures = {
|
||||||
|
workspaceDir: path.join(testDir, 'workspace'),
|
||||||
|
exampleArtifact: {
|
||||||
|
path: path.join(testDir, 'artifact.zip'),
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
path: 'hello.txt',
|
||||||
|
content: 'Hello World!'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'goodbye.txt',
|
||||||
|
content: 'Goodbye World!'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
artifactID: 1234,
|
||||||
|
repositoryOwner: 'actions',
|
||||||
|
repositoryName: 'toolkit',
|
||||||
|
token: 'ghp_1234567890',
|
||||||
|
blobStorageUrl: 'https://blob-storage.local?signed=true'
|
||||||
|
}
|
||||||
|
|
||||||
|
jest.mock('@actions/github', () => ({
|
||||||
|
getOctokit: jest.fn().mockReturnValue({
|
||||||
|
rest: {
|
||||||
|
actions: {
|
||||||
|
downloadArtifact: jest.fn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('@actions/http-client')
|
||||||
|
|
||||||
|
// Create a zip archive with the contents of the example artifact
|
||||||
|
const createTestArchive = async (): Promise<void> => {
|
||||||
|
const archive = archiver('zip', {
|
||||||
|
zlib: {level: 9}
|
||||||
|
})
|
||||||
|
for (const file of fixtures.exampleArtifact.files) {
|
||||||
|
archive.append(file.content, {name: file.path})
|
||||||
|
}
|
||||||
|
archive.finalize()
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
archive.pipe(fs.createWriteStream(fixtures.exampleArtifact.path))
|
||||||
|
archive.on('error', reject)
|
||||||
|
archive.on('finish', resolve)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectExtractedArchive = async (dir: string): Promise<void> => {
|
||||||
|
for (const file of fixtures.exampleArtifact.files) {
|
||||||
|
const filePath = path.join(dir, file.path)
|
||||||
|
expect(fs.readFileSync(filePath, 'utf8')).toEqual(file.content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('download-artifact', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.spyOn(core, 'debug').mockImplementation(() => {})
|
||||||
|
jest.spyOn(core, 'info').mockImplementation(() => {})
|
||||||
|
jest.spyOn(core, 'warning').mockImplementation(() => {})
|
||||||
|
|
||||||
|
await fs.promises.mkdir(testDir, {recursive: true})
|
||||||
|
await createTestArchive()
|
||||||
|
|
||||||
|
process.env['GITHUB_WORKSPACE'] = fixtures.workspaceDir
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.restoreAllMocks()
|
||||||
|
await fs.promises.rm(testDir, {recursive: true})
|
||||||
|
delete process.env['GITHUB_WORKSPACE']
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should successfully download an artifact to $GITHUB_WORKSPACE', async () => {
|
||||||
|
const downloadArtifactMock = github.getOctokit(fixtures.token).rest.actions
|
||||||
|
.downloadArtifact as MockedDownloadArtifact
|
||||||
|
downloadArtifactMock.mockResolvedValueOnce({
|
||||||
|
headers: {
|
||||||
|
location: fixtures.blobStorageUrl
|
||||||
|
},
|
||||||
|
status: 302,
|
||||||
|
url: '',
|
||||||
|
data: Buffer.from('')
|
||||||
|
})
|
||||||
|
|
||||||
|
const getMock = jest.fn(() => {
|
||||||
|
const message = new http.IncomingMessage(new net.Socket())
|
||||||
|
message.statusCode = 200
|
||||||
|
message.push(fs.readFileSync(fixtures.exampleArtifact.path))
|
||||||
|
return {
|
||||||
|
message
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const httpClientMock = (HttpClient as jest.Mock).mockImplementation(() => {
|
||||||
|
return {
|
||||||
|
get: getMock
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await downloadArtifact(
|
||||||
|
fixtures.artifactID,
|
||||||
|
fixtures.repositoryOwner,
|
||||||
|
fixtures.repositoryName,
|
||||||
|
fixtures.token
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(downloadArtifactMock).toHaveBeenCalledWith({
|
||||||
|
owner: fixtures.repositoryOwner,
|
||||||
|
repo: fixtures.repositoryName,
|
||||||
|
artifact_id: fixtures.artifactID,
|
||||||
|
archive_format: 'zip',
|
||||||
|
request: {
|
||||||
|
redirect: 'manual'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
expect(httpClientMock).toHaveBeenCalledWith(getUserAgentString())
|
||||||
|
expect(getMock).toHaveBeenCalledWith(fixtures.blobStorageUrl)
|
||||||
|
|
||||||
|
expectExtractedArchive(fixtures.workspaceDir)
|
||||||
|
|
||||||
|
expect(response.success).toBe(true)
|
||||||
|
expect(response.downloadPath).toBe(fixtures.workspaceDir)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should successfully download an artifact to user defined path', async () => {
|
||||||
|
const customPath = path.join(testDir, 'custom')
|
||||||
|
|
||||||
|
const downloadArtifactMock = github.getOctokit(fixtures.token).rest.actions
|
||||||
|
.downloadArtifact as MockedDownloadArtifact
|
||||||
|
downloadArtifactMock.mockResolvedValueOnce({
|
||||||
|
headers: {
|
||||||
|
location: fixtures.blobStorageUrl
|
||||||
|
},
|
||||||
|
status: 302,
|
||||||
|
url: '',
|
||||||
|
data: Buffer.from('')
|
||||||
|
})
|
||||||
|
|
||||||
|
const getMock = jest.fn(() => {
|
||||||
|
const message = new http.IncomingMessage(new net.Socket())
|
||||||
|
message.statusCode = 200
|
||||||
|
message.push(fs.readFileSync(fixtures.exampleArtifact.path))
|
||||||
|
return {
|
||||||
|
message
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const httpClientMock = (HttpClient as jest.Mock).mockImplementation(() => {
|
||||||
|
return {
|
||||||
|
get: getMock
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await downloadArtifact(
|
||||||
|
fixtures.artifactID,
|
||||||
|
fixtures.repositoryOwner,
|
||||||
|
fixtures.repositoryName,
|
||||||
|
fixtures.token,
|
||||||
|
{
|
||||||
|
path: customPath
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(downloadArtifactMock).toHaveBeenCalledWith({
|
||||||
|
owner: fixtures.repositoryOwner,
|
||||||
|
repo: fixtures.repositoryName,
|
||||||
|
artifact_id: fixtures.artifactID,
|
||||||
|
archive_format: 'zip',
|
||||||
|
request: {
|
||||||
|
redirect: 'manual'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
expect(httpClientMock).toHaveBeenCalledWith(getUserAgentString())
|
||||||
|
expect(getMock).toHaveBeenCalledWith(fixtures.blobStorageUrl)
|
||||||
|
|
||||||
|
expectExtractedArchive(customPath)
|
||||||
|
|
||||||
|
expect(response.success).toBe(true)
|
||||||
|
expect(response.downloadPath).toBe(customPath)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fail if download artifact API does not respond with location', async () => {
|
||||||
|
const downloadArtifactMock = github.getOctokit(fixtures.token).rest.actions
|
||||||
|
.downloadArtifact as MockedDownloadArtifact
|
||||||
|
downloadArtifactMock.mockResolvedValueOnce({
|
||||||
|
headers: {},
|
||||||
|
status: 302,
|
||||||
|
url: '',
|
||||||
|
data: Buffer.from('')
|
||||||
|
})
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
downloadArtifact(
|
||||||
|
fixtures.artifactID,
|
||||||
|
fixtures.repositoryOwner,
|
||||||
|
fixtures.repositoryName,
|
||||||
|
fixtures.token
|
||||||
|
)
|
||||||
|
).rejects.toBeInstanceOf(Error)
|
||||||
|
|
||||||
|
expect(downloadArtifactMock).toHaveBeenCalledWith({
|
||||||
|
owner: fixtures.repositoryOwner,
|
||||||
|
repo: fixtures.repositoryName,
|
||||||
|
artifact_id: fixtures.artifactID,
|
||||||
|
archive_format: 'zip',
|
||||||
|
request: {
|
||||||
|
redirect: 'manual'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fail if blob storage response is non-200', async () => {
|
||||||
|
const downloadArtifactMock = github.getOctokit(fixtures.token).rest.actions
|
||||||
|
.downloadArtifact as MockedDownloadArtifact
|
||||||
|
downloadArtifactMock.mockResolvedValueOnce({
|
||||||
|
headers: {
|
||||||
|
location: fixtures.blobStorageUrl
|
||||||
|
},
|
||||||
|
status: 302,
|
||||||
|
url: '',
|
||||||
|
data: Buffer.from('')
|
||||||
|
})
|
||||||
|
|
||||||
|
const getMock = jest.fn(() => {
|
||||||
|
const message = new http.IncomingMessage(new net.Socket())
|
||||||
|
message.statusCode = 500
|
||||||
|
message.push('Internal Server Error')
|
||||||
|
return {
|
||||||
|
message
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const httpClientMock = (HttpClient as jest.Mock).mockImplementation(() => {
|
||||||
|
return {
|
||||||
|
get: getMock
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
downloadArtifact(
|
||||||
|
fixtures.artifactID,
|
||||||
|
fixtures.repositoryOwner,
|
||||||
|
fixtures.repositoryName,
|
||||||
|
fixtures.token
|
||||||
|
)
|
||||||
|
).rejects.toBeInstanceOf(Error)
|
||||||
|
|
||||||
|
expect(downloadArtifactMock).toHaveBeenCalledWith({
|
||||||
|
owner: fixtures.repositoryOwner,
|
||||||
|
repo: fixtures.repositoryName,
|
||||||
|
artifact_id: fixtures.artifactID,
|
||||||
|
archive_format: 'zip',
|
||||||
|
request: {
|
||||||
|
redirect: 'manual'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
expect(httpClientMock).toHaveBeenCalledWith(getUserAgentString())
|
||||||
|
expect(getMock).toHaveBeenCalledWith(fixtures.blobStorageUrl)
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,65 @@
|
||||||
|
import {Timestamp} from '../src/generated'
|
||||||
|
import * as retention from '../src/internal/upload/retention'
|
||||||
|
|
||||||
|
describe('retention', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
delete process.env['GITHUB_RETENTION_DAYS']
|
||||||
|
})
|
||||||
|
it('should return the inputted retention days if it is less than the max retention days', () => {
|
||||||
|
// setup
|
||||||
|
const mockDate = new Date('2020-01-01')
|
||||||
|
jest.useFakeTimers().setSystemTime(mockDate)
|
||||||
|
process.env['GITHUB_RETENTION_DAYS'] = '90'
|
||||||
|
|
||||||
|
const exp = retention.getExpiration(30)
|
||||||
|
|
||||||
|
expect(exp).toBeDefined()
|
||||||
|
if (exp) {
|
||||||
|
const expDate = Timestamp.toDate(exp)
|
||||||
|
const expected = new Date()
|
||||||
|
expected.setDate(expected.getDate() + 30)
|
||||||
|
|
||||||
|
expect(expDate).toEqual(expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return the max retention days if the inputted retention days is greater than the max retention days', () => {
|
||||||
|
// setup
|
||||||
|
const mockDate = new Date('2020-01-01')
|
||||||
|
jest.useFakeTimers().setSystemTime(mockDate)
|
||||||
|
process.env['GITHUB_RETENTION_DAYS'] = '90'
|
||||||
|
|
||||||
|
const exp = retention.getExpiration(120)
|
||||||
|
|
||||||
|
expect(exp).toBeDefined()
|
||||||
|
if (exp) {
|
||||||
|
const expDate = Timestamp.toDate(exp) // we check whether exp is defined above
|
||||||
|
const expected = new Date()
|
||||||
|
expected.setDate(expected.getDate() + 90)
|
||||||
|
|
||||||
|
expect(expDate).toEqual(expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return undefined if the inputted retention days is undefined', () => {
|
||||||
|
const exp = retention.getExpiration()
|
||||||
|
expect(exp).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return the inputted retention days if there is no max retention days', () => {
|
||||||
|
// setup
|
||||||
|
const mockDate = new Date('2020-01-01')
|
||||||
|
jest.useFakeTimers().setSystemTime(mockDate)
|
||||||
|
|
||||||
|
const exp = retention.getExpiration(30)
|
||||||
|
|
||||||
|
expect(exp).toBeDefined()
|
||||||
|
if (exp) {
|
||||||
|
const expDate = Timestamp.toDate(exp) // we check whether exp is defined above
|
||||||
|
const expected = new Date()
|
||||||
|
expected.setDate(expected.getDate() + 30)
|
||||||
|
|
||||||
|
expect(expDate).toEqual(expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,362 @@
|
||||||
|
import * as core from '@actions/core'
|
||||||
|
import * as uploadZipSpecification from '../src/internal/upload/upload-zip-specification'
|
||||||
|
import * as zip from '../src/internal/upload/zip'
|
||||||
|
import * as util from '../src/internal/shared/util'
|
||||||
|
import * as retention from '../src/internal/upload/retention'
|
||||||
|
import * as config from '../src/internal/shared/config'
|
||||||
|
import {Timestamp, ArtifactServiceClientJSON} from '../src/generated'
|
||||||
|
import * as blobUpload from '../src/internal/upload/blob-upload'
|
||||||
|
import {uploadArtifact} from '../src/internal/upload/upload-artifact'
|
||||||
|
|
||||||
|
describe('upload-artifact', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// mock all output so that there is less noise when running tests
|
||||||
|
jest.spyOn(console, 'log').mockImplementation(() => {})
|
||||||
|
jest.spyOn(core, 'debug').mockImplementation(() => {})
|
||||||
|
jest.spyOn(core, 'info').mockImplementation(() => {})
|
||||||
|
jest.spyOn(core, 'warning').mockImplementation(() => {})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should successfully upload an artifact', () => {
|
||||||
|
const mockDate = new Date('2020-01-01')
|
||||||
|
jest
|
||||||
|
.spyOn(uploadZipSpecification, 'validateRootDirectory')
|
||||||
|
.mockReturnValue()
|
||||||
|
jest
|
||||||
|
.spyOn(uploadZipSpecification, 'getUploadZipSpecification')
|
||||||
|
.mockReturnValue([
|
||||||
|
{
|
||||||
|
sourcePath: '/home/user/files/plz-upload/file1.txt',
|
||||||
|
destinationPath: 'file1.txt'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sourcePath: '/home/user/files/plz-upload/file2.txt',
|
||||||
|
destinationPath: 'file2.txt'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sourcePath: '/home/user/files/plz-upload/dir/file3.txt',
|
||||||
|
destinationPath: 'dir/file3.txt'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(zip, 'createZipUploadStream')
|
||||||
|
.mockReturnValue(Promise.resolve(new zip.ZipUploadStream(1)))
|
||||||
|
jest.spyOn(util, 'getBackendIdsFromToken').mockReturnValue({
|
||||||
|
workflowRunBackendId: '1234',
|
||||||
|
workflowJobRunBackendId: '5678'
|
||||||
|
})
|
||||||
|
jest
|
||||||
|
.spyOn(retention, 'getExpiration')
|
||||||
|
.mockReturnValue(Timestamp.fromDate(mockDate))
|
||||||
|
jest
|
||||||
|
.spyOn(ArtifactServiceClientJSON.prototype, 'CreateArtifact')
|
||||||
|
.mockReturnValue(
|
||||||
|
Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
signedUploadUrl: 'https://signed-upload-url.com'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
jest.spyOn(blobUpload, 'uploadZipToBlobStorage').mockReturnValue(
|
||||||
|
Promise.resolve({
|
||||||
|
isSuccess: true,
|
||||||
|
uploadSize: 1234,
|
||||||
|
md5Hash: 'test-md5-hash'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
jest
|
||||||
|
.spyOn(ArtifactServiceClientJSON.prototype, 'FinalizeArtifact')
|
||||||
|
.mockReturnValue(Promise.resolve({ok: true, artifactId: '1'}))
|
||||||
|
|
||||||
|
// ArtifactHttpClient mocks
|
||||||
|
jest.spyOn(config, 'getRuntimeToken').mockReturnValue('test-token')
|
||||||
|
jest
|
||||||
|
.spyOn(config, 'getResultsServiceUrl')
|
||||||
|
.mockReturnValue('https://test-url.com')
|
||||||
|
|
||||||
|
const uploadResp = uploadArtifact(
|
||||||
|
'test-artifact',
|
||||||
|
[
|
||||||
|
'/home/user/files/plz-upload/file1.txt',
|
||||||
|
'/home/user/files/plz-upload/file2.txt',
|
||||||
|
'/home/user/files/plz-upload/dir/file3.txt'
|
||||||
|
],
|
||||||
|
'/home/user/files/plz-upload'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(uploadResp).resolves.toEqual({success: true, size: 1234, id: 1})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw an error if the root directory is invalid', () => {
|
||||||
|
jest
|
||||||
|
.spyOn(uploadZipSpecification, 'validateRootDirectory')
|
||||||
|
.mockImplementation(() => {
|
||||||
|
throw new Error('Invalid root directory')
|
||||||
|
})
|
||||||
|
|
||||||
|
const uploadResp = uploadArtifact(
|
||||||
|
'test-artifact',
|
||||||
|
[
|
||||||
|
'/home/user/files/plz-upload/file1.txt',
|
||||||
|
'/home/user/files/plz-upload/file2.txt',
|
||||||
|
'/home/user/files/plz-upload/dir/file3.txt'
|
||||||
|
],
|
||||||
|
'/home/user/files/plz-upload'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(uploadResp).rejects.toThrow('Invalid root directory')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false if there are no files to upload', () => {
|
||||||
|
jest
|
||||||
|
.spyOn(uploadZipSpecification, 'validateRootDirectory')
|
||||||
|
.mockReturnValue()
|
||||||
|
jest
|
||||||
|
.spyOn(uploadZipSpecification, 'getUploadZipSpecification')
|
||||||
|
.mockReturnValue([])
|
||||||
|
|
||||||
|
const uploadResp = uploadArtifact(
|
||||||
|
'test-artifact',
|
||||||
|
[
|
||||||
|
'/home/user/files/plz-upload/file1.txt',
|
||||||
|
'/home/user/files/plz-upload/file2.txt',
|
||||||
|
'/home/user/files/plz-upload/dir/file3.txt'
|
||||||
|
],
|
||||||
|
'/home/user/files/plz-upload'
|
||||||
|
)
|
||||||
|
expect(uploadResp).resolves.toEqual({success: false})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false if no backend IDs are found', () => {
|
||||||
|
jest
|
||||||
|
.spyOn(uploadZipSpecification, 'validateRootDirectory')
|
||||||
|
.mockReturnValue()
|
||||||
|
jest
|
||||||
|
.spyOn(uploadZipSpecification, 'getUploadZipSpecification')
|
||||||
|
.mockReturnValue([
|
||||||
|
{
|
||||||
|
sourcePath: '/home/user/files/plz-upload/file1.txt',
|
||||||
|
destinationPath: 'file1.txt'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sourcePath: '/home/user/files/plz-upload/file2.txt',
|
||||||
|
destinationPath: 'file2.txt'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sourcePath: '/home/user/files/plz-upload/dir/file3.txt',
|
||||||
|
destinationPath: 'dir/file3.txt'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(zip, 'createZipUploadStream')
|
||||||
|
.mockReturnValue(Promise.resolve(new zip.ZipUploadStream(1)))
|
||||||
|
jest
|
||||||
|
.spyOn(util, 'getBackendIdsFromToken')
|
||||||
|
.mockReturnValue({workflowRunBackendId: '', workflowJobRunBackendId: ''})
|
||||||
|
|
||||||
|
const uploadResp = uploadArtifact(
|
||||||
|
'test-artifact',
|
||||||
|
[
|
||||||
|
'/home/user/files/plz-upload/file1.txt',
|
||||||
|
'/home/user/files/plz-upload/file2.txt',
|
||||||
|
'/home/user/files/plz-upload/dir/file3.txt'
|
||||||
|
],
|
||||||
|
'/home/user/files/plz-upload'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(uploadResp).resolves.toEqual({success: false})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false if the creation request fails', () => {
|
||||||
|
const mockDate = new Date('2020-01-01')
|
||||||
|
jest
|
||||||
|
.spyOn(uploadZipSpecification, 'validateRootDirectory')
|
||||||
|
.mockReturnValue()
|
||||||
|
jest
|
||||||
|
.spyOn(uploadZipSpecification, 'getUploadZipSpecification')
|
||||||
|
.mockReturnValue([
|
||||||
|
{
|
||||||
|
sourcePath: '/home/user/files/plz-upload/file1.txt',
|
||||||
|
destinationPath: 'file1.txt'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sourcePath: '/home/user/files/plz-upload/file2.txt',
|
||||||
|
destinationPath: 'file2.txt'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sourcePath: '/home/user/files/plz-upload/dir/file3.txt',
|
||||||
|
destinationPath: 'dir/file3.txt'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(zip, 'createZipUploadStream')
|
||||||
|
.mockReturnValue(Promise.resolve(new zip.ZipUploadStream(1)))
|
||||||
|
jest.spyOn(util, 'getBackendIdsFromToken').mockReturnValue({
|
||||||
|
workflowRunBackendId: '1234',
|
||||||
|
workflowJobRunBackendId: '5678'
|
||||||
|
})
|
||||||
|
jest
|
||||||
|
.spyOn(retention, 'getExpiration')
|
||||||
|
.mockReturnValue(Timestamp.fromDate(mockDate))
|
||||||
|
jest
|
||||||
|
.spyOn(ArtifactServiceClientJSON.prototype, 'CreateArtifact')
|
||||||
|
.mockReturnValue(Promise.resolve({ok: false, signedUploadUrl: ''}))
|
||||||
|
|
||||||
|
// ArtifactHttpClient mocks
|
||||||
|
jest.spyOn(config, 'getRuntimeToken').mockReturnValue('test-token')
|
||||||
|
jest
|
||||||
|
.spyOn(config, 'getResultsServiceUrl')
|
||||||
|
.mockReturnValue('https://test-url.com')
|
||||||
|
|
||||||
|
const uploadResp = uploadArtifact(
|
||||||
|
'test-artifact',
|
||||||
|
[
|
||||||
|
'/home/user/files/plz-upload/file1.txt',
|
||||||
|
'/home/user/files/plz-upload/file2.txt',
|
||||||
|
'/home/user/files/plz-upload/dir/file3.txt'
|
||||||
|
],
|
||||||
|
'/home/user/files/plz-upload'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(uploadResp).resolves.toEqual({success: false})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false if blob storage upload is unsuccessful', () => {
|
||||||
|
const mockDate = new Date('2020-01-01')
|
||||||
|
jest
|
||||||
|
.spyOn(uploadZipSpecification, 'validateRootDirectory')
|
||||||
|
.mockReturnValue()
|
||||||
|
jest
|
||||||
|
.spyOn(uploadZipSpecification, 'getUploadZipSpecification')
|
||||||
|
.mockReturnValue([
|
||||||
|
{
|
||||||
|
sourcePath: '/home/user/files/plz-upload/file1.txt',
|
||||||
|
destinationPath: 'file1.txt'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sourcePath: '/home/user/files/plz-upload/file2.txt',
|
||||||
|
destinationPath: 'file2.txt'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sourcePath: '/home/user/files/plz-upload/dir/file3.txt',
|
||||||
|
destinationPath: 'dir/file3.txt'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(zip, 'createZipUploadStream')
|
||||||
|
.mockReturnValue(Promise.resolve(new zip.ZipUploadStream(1)))
|
||||||
|
jest.spyOn(util, 'getBackendIdsFromToken').mockReturnValue({
|
||||||
|
workflowRunBackendId: '1234',
|
||||||
|
workflowJobRunBackendId: '5678'
|
||||||
|
})
|
||||||
|
jest
|
||||||
|
.spyOn(retention, 'getExpiration')
|
||||||
|
.mockReturnValue(Timestamp.fromDate(mockDate))
|
||||||
|
jest
|
||||||
|
.spyOn(ArtifactServiceClientJSON.prototype, 'CreateArtifact')
|
||||||
|
.mockReturnValue(
|
||||||
|
Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
signedUploadUrl: 'https://signed-upload-url.com'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
jest
|
||||||
|
.spyOn(blobUpload, 'uploadZipToBlobStorage')
|
||||||
|
.mockReturnValue(Promise.resolve({isSuccess: false}))
|
||||||
|
|
||||||
|
// ArtifactHttpClient mocks
|
||||||
|
jest.spyOn(config, 'getRuntimeToken').mockReturnValue('test-token')
|
||||||
|
jest
|
||||||
|
.spyOn(config, 'getResultsServiceUrl')
|
||||||
|
.mockReturnValue('https://test-url.com')
|
||||||
|
|
||||||
|
const uploadResp = uploadArtifact(
|
||||||
|
'test-artifact',
|
||||||
|
[
|
||||||
|
'/home/user/files/plz-upload/file1.txt',
|
||||||
|
'/home/user/files/plz-upload/file2.txt',
|
||||||
|
'/home/user/files/plz-upload/dir/file3.txt'
|
||||||
|
],
|
||||||
|
'/home/user/files/plz-upload'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(uploadResp).resolves.toEqual({success: false})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false if finalize artifact fails', () => {
|
||||||
|
const mockDate = new Date('2020-01-01')
|
||||||
|
jest
|
||||||
|
.spyOn(uploadZipSpecification, 'validateRootDirectory')
|
||||||
|
.mockReturnValue()
|
||||||
|
jest
|
||||||
|
.spyOn(uploadZipSpecification, 'getUploadZipSpecification')
|
||||||
|
.mockReturnValue([
|
||||||
|
{
|
||||||
|
sourcePath: '/home/user/files/plz-upload/file1.txt',
|
||||||
|
destinationPath: 'file1.txt'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sourcePath: '/home/user/files/plz-upload/file2.txt',
|
||||||
|
destinationPath: 'file2.txt'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sourcePath: '/home/user/files/plz-upload/dir/file3.txt',
|
||||||
|
destinationPath: 'dir/file3.txt'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(zip, 'createZipUploadStream')
|
||||||
|
.mockReturnValue(Promise.resolve(new zip.ZipUploadStream(1)))
|
||||||
|
jest.spyOn(util, 'getBackendIdsFromToken').mockReturnValue({
|
||||||
|
workflowRunBackendId: '1234',
|
||||||
|
workflowJobRunBackendId: '5678'
|
||||||
|
})
|
||||||
|
jest
|
||||||
|
.spyOn(retention, 'getExpiration')
|
||||||
|
.mockReturnValue(Timestamp.fromDate(mockDate))
|
||||||
|
jest
|
||||||
|
.spyOn(ArtifactServiceClientJSON.prototype, 'CreateArtifact')
|
||||||
|
.mockReturnValue(
|
||||||
|
Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
signedUploadUrl: 'https://signed-upload-url.com'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
jest.spyOn(blobUpload, 'uploadZipToBlobStorage').mockReturnValue(
|
||||||
|
Promise.resolve({
|
||||||
|
isSuccess: true,
|
||||||
|
uploadSize: 1234,
|
||||||
|
md5Hash: 'test-md5-hash'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
jest
|
||||||
|
.spyOn(ArtifactServiceClientJSON.prototype, 'FinalizeArtifact')
|
||||||
|
.mockReturnValue(Promise.resolve({ok: false, artifactId: ''}))
|
||||||
|
|
||||||
|
// ArtifactHttpClient mocks
|
||||||
|
jest.spyOn(config, 'getRuntimeToken').mockReturnValue('test-token')
|
||||||
|
jest
|
||||||
|
.spyOn(config, 'getResultsServiceUrl')
|
||||||
|
.mockReturnValue('https://test-url.com')
|
||||||
|
|
||||||
|
const uploadResp = uploadArtifact(
|
||||||
|
'test-artifact',
|
||||||
|
[
|
||||||
|
'/home/user/files/plz-upload/file1.txt',
|
||||||
|
'/home/user/files/plz-upload/file2.txt',
|
||||||
|
'/home/user/files/plz-upload/dir/file3.txt'
|
||||||
|
],
|
||||||
|
'/home/user/files/plz-upload'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(uploadResp).resolves.toEqual({success: false})
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,62 @@
|
||||||
|
import * as config from '../src/internal/shared/config'
|
||||||
|
import * as util from '../src/internal/shared/util'
|
||||||
|
|
||||||
|
describe('get-backend-ids-from-token', () => {
|
||||||
|
it('should return backend ids when the token is valid', () => {
|
||||||
|
jest
|
||||||
|
.spyOn(config, 'getRuntimeToken')
|
||||||
|
.mockReturnValue(
|
||||||
|
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwic2NwIjoiQWN0aW9ucy5FeGFtcGxlIEFjdGlvbnMuQW5vdGhlckV4YW1wbGU6dGVzdCBBY3Rpb25zLlJlc3VsdHM6Y2U3ZjU0YzctNjFjNy00YWFlLTg4N2YtMzBkYTQ3NWY1ZjFhOmNhMzk1MDg1LTA0MGEtNTI2Yi0yY2U4LWJkYzg1ZjY5Mjc3NCIsImlhdCI6MTUxNjIzOTAyMn0.XYnI_wHPBlUi1mqYveJnnkJhp4dlFjqxzRmISPsqfw8'
|
||||||
|
)
|
||||||
|
|
||||||
|
const backendIds = util.getBackendIdsFromToken()
|
||||||
|
expect(backendIds.workflowRunBackendId).toBe(
|
||||||
|
'ce7f54c7-61c7-4aae-887f-30da475f5f1a'
|
||||||
|
)
|
||||||
|
expect(backendIds.workflowJobRunBackendId).toBe(
|
||||||
|
'ca395085-040a-526b-2ce8-bdc85f692774'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should throw an error when the token doesn't have the right scope", () => {
|
||||||
|
jest
|
||||||
|
.spyOn(config, 'getRuntimeToken')
|
||||||
|
.mockReturnValue(
|
||||||
|
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwic2NwIjoiQWN0aW9ucy5FeGFtcGxlIEFjdGlvbnMuQW5vdGhlckV4YW1wbGU6dGVzdCIsImlhdCI6MTUxNjIzOTAyMn0.K0IEoULZteGevF38G94xiaA8zcZ5UlKWfGfqE6q3dhw'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(util.getBackendIdsFromToken).toThrowError(
|
||||||
|
'Failed to get backend IDs: The provided JWT token is invalid'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw an error when the token has a malformed scope', () => {
|
||||||
|
jest
|
||||||
|
.spyOn(config, 'getRuntimeToken')
|
||||||
|
.mockReturnValue(
|
||||||
|
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwic2NwIjoiQWN0aW9ucy5FeGFtcGxlIEFjdGlvbnMuQW5vdGhlckV4YW1wbGU6dGVzdCBBY3Rpb25zLlJlc3VsdHM6Y2U3ZjU0YzctNjFjNy00YWFlLTg4N2YtMzBkYTQ3NWY1ZjFhIiwiaWF0IjoxNTE2MjM5MDIyfQ.7D0_LRfRFRZFImHQ7GxH2S6ZyFjjZ5U0ujjGCfle1XE'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(util.getBackendIdsFromToken).toThrowError(
|
||||||
|
'Failed to get backend IDs: The provided JWT token is invalid'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw an error when the token is in an invalid format', () => {
|
||||||
|
jest.spyOn(config, 'getRuntimeToken').mockReturnValue('token')
|
||||||
|
|
||||||
|
expect(util.getBackendIdsFromToken).toThrowError('Invalid token specified')
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should throw an error when the token doesn't have the right field", () => {
|
||||||
|
jest
|
||||||
|
.spyOn(config, 'getRuntimeToken')
|
||||||
|
.mockReturnValue(
|
||||||
|
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(util.getBackendIdsFromToken).toThrowError(
|
||||||
|
'Failed to get backend IDs: The provided JWT token is invalid'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
File diff suppressed because it is too large
Load Diff
|
@ -10,8 +10,8 @@
|
||||||
],
|
],
|
||||||
"homepage": "https://github.com/actions/toolkit/tree/main/packages/artifact",
|
"homepage": "https://github.com/actions/toolkit/tree/main/packages/artifact",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "lib/artifact-client.js",
|
"main": "lib/artifact.js",
|
||||||
"types": "lib/artifact-client.d.ts",
|
"types": "lib/artifact.d.ts",
|
||||||
"directories": {
|
"directories": {
|
||||||
"lib": "lib",
|
"lib": "lib",
|
||||||
"test": "__tests__"
|
"test": "__tests__"
|
||||||
|
@ -40,12 +40,23 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.10.0",
|
"@actions/core": "^1.10.0",
|
||||||
|
"@actions/github": "^5.1.1",
|
||||||
"@actions/http-client": "^2.1.0",
|
"@actions/http-client": "^2.1.0",
|
||||||
|
"@azure/storage-blob": "^12.15.0",
|
||||||
|
"@octokit/core": "^3.5.1",
|
||||||
|
"@octokit/plugin-request-log": "^1.0.4",
|
||||||
|
"@octokit/plugin-retry": "^3.0.9",
|
||||||
|
"@octokit/request-error": "^5.0.0",
|
||||||
"@protobuf-ts/plugin": "^2.2.3-alpha.1",
|
"@protobuf-ts/plugin": "^2.2.3-alpha.1",
|
||||||
"twirp-ts": "^2.5.0"
|
"@types/unzipper": "^0.10.6",
|
||||||
|
"archiver": "^5.3.1",
|
||||||
|
"crypto": "^1.0.1",
|
||||||
|
"jwt-decode": "^3.1.2",
|
||||||
|
"twirp-ts": "^2.5.0",
|
||||||
|
"unzipper": "^0.10.14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/tmp": "^0.2.1",
|
"@types/archiver": "^5.3.2",
|
||||||
"typescript": "^4.3.0"
|
"typescript": "^5.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import {ArtifactClient, Client} from './internal/client'
|
import {ArtifactClient, Client} from './internal/client'
|
||||||
import {UploadOptions} from './internal/upload/upload-options'
|
|
||||||
import {UploadResponse} from './internal/upload/upload-response'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exported functionality that we want to expose for any users of @actions/artifact
|
* Exported functionality that we want to expose for any users of @actions/artifact
|
||||||
*/
|
*/
|
||||||
export {ArtifactClient, UploadOptions, UploadResponse}
|
export * from './internal/shared/interfaces'
|
||||||
|
export {ArtifactClient}
|
||||||
|
|
||||||
export function create(): ArtifactClient {
|
export function create(): ArtifactClient {
|
||||||
return Client.create()
|
return Client.create()
|
||||||
|
|
|
@ -1,16 +1,27 @@
|
||||||
import {UploadOptions} from './upload/upload-options'
|
import {warning} from '@actions/core'
|
||||||
import {UploadResponse} from './upload/upload-response'
|
import {isGhes} from './shared/config'
|
||||||
|
import {
|
||||||
|
UploadOptions,
|
||||||
|
UploadResponse,
|
||||||
|
DownloadArtifactOptions,
|
||||||
|
GetArtifactResponse,
|
||||||
|
ListArtifactsResponse,
|
||||||
|
DownloadArtifactResponse
|
||||||
|
} from './shared/interfaces'
|
||||||
import {uploadArtifact} from './upload/upload-artifact'
|
import {uploadArtifact} from './upload/upload-artifact'
|
||||||
|
import {downloadArtifact} from './download/download-artifact'
|
||||||
|
import {getArtifact} from './find/get-artifact'
|
||||||
|
import {listArtifacts} from './find/list-artifacts'
|
||||||
|
|
||||||
export interface ArtifactClient {
|
export interface ArtifactClient {
|
||||||
/**
|
/**
|
||||||
* Uploads an artifact
|
* Uploads an artifact
|
||||||
*
|
*
|
||||||
* @param name the name of the artifact, required
|
* @param name The name of the artifact, required
|
||||||
* @param files a list of absolute or relative paths that denote what files should be uploaded
|
* @param files A list of absolute or relative paths that denote what files should be uploaded
|
||||||
* @param rootDirectory an absolute or relative file path that denotes the root parent directory of the files being uploaded
|
* @param rootDirectory An absolute or relative file path that denotes the root parent directory of the files being uploaded
|
||||||
* @param options extra options for customizing the upload behavior
|
* @param options Extra options for customizing the upload behavior
|
||||||
* @returns single UploadInfo object
|
* @returns single UploadResponse object
|
||||||
*/
|
*/
|
||||||
uploadArtifact(
|
uploadArtifact(
|
||||||
name: string,
|
name: string,
|
||||||
|
@ -19,7 +30,64 @@ export interface ArtifactClient {
|
||||||
options?: UploadOptions
|
options?: UploadOptions
|
||||||
): Promise<UploadResponse>
|
): Promise<UploadResponse>
|
||||||
|
|
||||||
// TODO Download functionality
|
/**
|
||||||
|
* Lists all artifacts that are part of a workflow run.
|
||||||
|
*
|
||||||
|
* This calls the public List-Artifacts API https://docs.github.com/en/rest/actions/artifacts?apiVersion=2022-11-28#list-workflow-run-artifacts
|
||||||
|
* Due to paginated responses from the public API. This function will return at most 1000 artifacts per workflow run (100 per page * maximum 10 calls)
|
||||||
|
*
|
||||||
|
* @param workflowRunId The workflow run id that the artifact belongs to
|
||||||
|
* @param repositoryOwner The owner of the repository that the artifact belongs to
|
||||||
|
* @param repositoryName The name of the repository that the artifact belongs to
|
||||||
|
* @param token A token with the appropriate permission to the repository to list artifacts
|
||||||
|
* @returns ListArtifactResponse object
|
||||||
|
*/
|
||||||
|
listArtifacts(
|
||||||
|
workflowRunId: number,
|
||||||
|
repositoryOwner: string,
|
||||||
|
repositoryName: string,
|
||||||
|
token: string
|
||||||
|
): Promise<ListArtifactsResponse>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds an artifact by name given a repository and workflow run id.
|
||||||
|
*
|
||||||
|
* This calls the public List-Artifacts API with a name filter https://docs.github.com/en/rest/actions/artifacts?apiVersion=2022-11-28#list-workflow-run-artifacts
|
||||||
|
* @actions/artifact > 2.0.0 does not allow for creating multiple artifacts with the same name in the same workflow run.
|
||||||
|
* It is possible to have multiple artifacts with the same name in the same workflow run by using old versions of upload-artifact (v1,v2 and v3) or @actions/artifact < v2.0.0
|
||||||
|
* If there are multiple artifacts with the same name in the same workflow run this function will return the first artifact that matches the name.
|
||||||
|
*
|
||||||
|
* @param artifactName The name of the artifact to find
|
||||||
|
* @param workflowRunId The workflow run id that the artifact belongs to
|
||||||
|
* @param repositoryOwner The owner of the repository that the artifact belongs to
|
||||||
|
* @param repositoryName The name of the repository that the artifact belongs to
|
||||||
|
* @param token A token with the appropriate permission to the repository to find the artifact
|
||||||
|
*/
|
||||||
|
getArtifact(
|
||||||
|
artifactName: string,
|
||||||
|
workflowRunId: number,
|
||||||
|
repositoryOwner: string,
|
||||||
|
repositoryName: string,
|
||||||
|
token: string
|
||||||
|
): Promise<GetArtifactResponse>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads an artifact and unzips the content
|
||||||
|
*
|
||||||
|
* @param artifactId The name of the artifact to download
|
||||||
|
* @param repositoryOwner The owner of the repository that the artifact belongs to
|
||||||
|
* @param repositoryName The name of the repository that the artifact belongs to
|
||||||
|
* @param token A token with the appropriate permission to the repository to download the artifact
|
||||||
|
* @param options Extra options that allow for the customization of the download behavior
|
||||||
|
* @returns single DownloadArtifactResponse object
|
||||||
|
*/
|
||||||
|
downloadArtifact(
|
||||||
|
artifactId: number,
|
||||||
|
repositoryOwner: string,
|
||||||
|
repositoryName: string,
|
||||||
|
token: string,
|
||||||
|
options?: DownloadArtifactOptions
|
||||||
|
): Promise<DownloadArtifactResponse>
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Client implements ArtifactClient {
|
export class Client implements ArtifactClient {
|
||||||
|
@ -31,7 +99,7 @@ export class Client implements ArtifactClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uploads an artifact
|
* Upload Artifact
|
||||||
*/
|
*/
|
||||||
async uploadArtifact(
|
async uploadArtifact(
|
||||||
name: string,
|
name: string,
|
||||||
|
@ -39,6 +107,151 @@ export class Client implements ArtifactClient {
|
||||||
rootDirectory: string,
|
rootDirectory: string,
|
||||||
options?: UploadOptions | undefined
|
options?: UploadOptions | undefined
|
||||||
): Promise<UploadResponse> {
|
): Promise<UploadResponse> {
|
||||||
return uploadArtifact(name, files, rootDirectory, options)
|
if (isGhes()) {
|
||||||
|
warning(
|
||||||
|
`@actions/artifact v2.0.0+ and upload-artifact@v4+ are not currently supported on GHES.`
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
success: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return uploadArtifact(name, files, rootDirectory, options)
|
||||||
|
} catch (error) {
|
||||||
|
warning(
|
||||||
|
`Artifact upload failed with error: ${error}.
|
||||||
|
|
||||||
|
Errors can be temporary, so please try again and optionally run the action with debug mode enabled for more information.
|
||||||
|
|
||||||
|
If the error persists, please check whether Actions is operating normally at [https://githubstatus.com](https://www.githubstatus.com).`
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
success: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download Artifact
|
||||||
|
*/
|
||||||
|
async downloadArtifact(
|
||||||
|
artifactId: number,
|
||||||
|
repositoryOwner: string,
|
||||||
|
repositoryName: string,
|
||||||
|
token: string,
|
||||||
|
options?: DownloadArtifactOptions
|
||||||
|
): Promise<DownloadArtifactResponse> {
|
||||||
|
if (isGhes()) {
|
||||||
|
warning(
|
||||||
|
`@actions/artifact v2.0.0+ and download-artifact@v4+ are not currently supported on GHES.`
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
success: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return downloadArtifact(
|
||||||
|
artifactId,
|
||||||
|
repositoryOwner,
|
||||||
|
repositoryName,
|
||||||
|
token,
|
||||||
|
options
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
warning(
|
||||||
|
`Artifact download failed with error: ${error}.
|
||||||
|
|
||||||
|
Errors can be temporary, so please try again and optionally run the action with debug mode enabled for more information.
|
||||||
|
|
||||||
|
If the error persists, please check whether Actions and API requests are operating normally at [https://githubstatus.com](https://www.githubstatus.com).`
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List Artifacts
|
||||||
|
*/
|
||||||
|
async listArtifacts(
|
||||||
|
workflowRunId: number,
|
||||||
|
repositoryOwner: string,
|
||||||
|
repositoryName: string,
|
||||||
|
token: string
|
||||||
|
): Promise<ListArtifactsResponse> {
|
||||||
|
if (isGhes()) {
|
||||||
|
warning(
|
||||||
|
`@actions/artifact v2.0.0+ and download-artifact@v4+ are not currently supported on GHES.`
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
artifacts: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return listArtifacts(
|
||||||
|
workflowRunId,
|
||||||
|
repositoryOwner,
|
||||||
|
repositoryName,
|
||||||
|
token
|
||||||
|
)
|
||||||
|
} catch (error: unknown) {
|
||||||
|
warning(
|
||||||
|
`Listing Artifacts failed with error: ${error}.
|
||||||
|
|
||||||
|
Errors can be temporary, so please try again and optionally run the action with debug mode enabled for more information.
|
||||||
|
|
||||||
|
If the error persists, please check whether Actions and API requests are operating normally at [https://githubstatus.com](https://www.githubstatus.com).`
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
artifacts: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Artifact
|
||||||
|
*/
|
||||||
|
async getArtifact(
|
||||||
|
artifactName: string,
|
||||||
|
workflowRunId: number,
|
||||||
|
repositoryOwner: string,
|
||||||
|
repositoryName: string,
|
||||||
|
token: string
|
||||||
|
): Promise<GetArtifactResponse> {
|
||||||
|
if (isGhes()) {
|
||||||
|
warning(
|
||||||
|
`@actions/artifact v2.0.0+ and download-artifact@v4+ are not currently supported on GHES.`
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
success: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return getArtifact(
|
||||||
|
artifactName,
|
||||||
|
workflowRunId,
|
||||||
|
repositoryOwner,
|
||||||
|
repositoryName,
|
||||||
|
token
|
||||||
|
)
|
||||||
|
} catch (error: unknown) {
|
||||||
|
warning(
|
||||||
|
`Fetching Artifact failed with error: ${error}.
|
||||||
|
|
||||||
|
Errors can be temporary, so please try again and optionally run the action with debug mode enabled for more information.
|
||||||
|
|
||||||
|
If the error persists, please check whether Actions and API requests are operating normally at [https://githubstatus.com](https://www.githubstatus.com).`
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
success: false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,101 @@
|
||||||
|
import fs from 'fs/promises'
|
||||||
|
import * as github from '@actions/github'
|
||||||
|
import * as core from '@actions/core'
|
||||||
|
import * as httpClient from '@actions/http-client'
|
||||||
|
import unzipper from 'unzipper'
|
||||||
|
import {
|
||||||
|
DownloadArtifactOptions,
|
||||||
|
DownloadArtifactResponse
|
||||||
|
} from '../shared/interfaces'
|
||||||
|
import {getUserAgentString} from '../shared/user-agent'
|
||||||
|
import {getGitHubWorkspaceDir} from '../shared/config'
|
||||||
|
|
||||||
|
const scrubQueryParameters = (url: string): string => {
|
||||||
|
const parsed = new URL(url)
|
||||||
|
parsed.search = ''
|
||||||
|
return parsed.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exists(path: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await fs.access(path)
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'ENOENT') {
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function streamExtract(url: string, directory: string): Promise<void> {
|
||||||
|
const client = new httpClient.HttpClient(getUserAgentString())
|
||||||
|
const response = await client.get(url)
|
||||||
|
|
||||||
|
if (response.message.statusCode !== 200) {
|
||||||
|
throw new Error(
|
||||||
|
`Unexpected HTTP response from blob storage: ${response.message.statusCode} ${response.message.statusMessage}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.message.pipe(unzipper.Extract({path: directory})).promise()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function downloadArtifact(
|
||||||
|
artifactId: number,
|
||||||
|
repositoryOwner: string,
|
||||||
|
repositoryName: string,
|
||||||
|
token: string,
|
||||||
|
options?: DownloadArtifactOptions
|
||||||
|
): Promise<DownloadArtifactResponse> {
|
||||||
|
const downloadPath = options?.path || getGitHubWorkspaceDir()
|
||||||
|
|
||||||
|
if (!(await exists(downloadPath))) {
|
||||||
|
core.debug(
|
||||||
|
`Artifact destination folder does not exist, creating: ${downloadPath}`
|
||||||
|
)
|
||||||
|
await fs.mkdir(downloadPath, {recursive: true})
|
||||||
|
} else {
|
||||||
|
core.debug(`Artifact destination folder already exists: ${downloadPath}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const api = github.getOctokit(token)
|
||||||
|
|
||||||
|
core.info(
|
||||||
|
`Downloading artifact '${artifactId}' from '${repositoryOwner}/${repositoryName}'`
|
||||||
|
)
|
||||||
|
|
||||||
|
const {headers, status} = await api.rest.actions.downloadArtifact({
|
||||||
|
owner: repositoryOwner,
|
||||||
|
repo: repositoryName,
|
||||||
|
artifact_id: artifactId,
|
||||||
|
archive_format: 'zip',
|
||||||
|
request: {
|
||||||
|
redirect: 'manual'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (status !== 302) {
|
||||||
|
throw new Error(`Unable to download artifact. Unexpected status: ${status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const {location} = headers
|
||||||
|
if (!location) {
|
||||||
|
throw new Error(`Unable to redirect to artifact download url`)
|
||||||
|
}
|
||||||
|
|
||||||
|
core.info(
|
||||||
|
`Redirecting to blob download url: ${scrubQueryParameters(location)}`
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
core.info(`Starting download of artifact to: ${downloadPath}`)
|
||||||
|
await streamExtract(location, downloadPath)
|
||||||
|
core.info(`Artifact download completed successfully.`)
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Unable to download and extract artifact: ${error.message}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {success: true, downloadPath}
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
import {GetArtifactResponse} from '../shared/interfaces'
|
||||||
|
import {getOctokit} from '@actions/github'
|
||||||
|
import {getUserAgentString} from '../shared/user-agent'
|
||||||
|
import {defaults as defaultGitHubOptions} from '@actions/github/lib/utils'
|
||||||
|
import {getRetryOptions} from './retry-options'
|
||||||
|
import {requestLog} from '@octokit/plugin-request-log'
|
||||||
|
import {retry} from '@octokit/plugin-retry'
|
||||||
|
import * as core from '@actions/core'
|
||||||
|
import {OctokitOptions} from '@octokit/core/dist-types/types'
|
||||||
|
|
||||||
|
export async function getArtifact(
|
||||||
|
artifactName: string,
|
||||||
|
workflowRunId: number,
|
||||||
|
repositoryOwner: string,
|
||||||
|
repositoryName: string,
|
||||||
|
token: string
|
||||||
|
): Promise<GetArtifactResponse> {
|
||||||
|
const [retryOpts, requestOpts] = getRetryOptions(defaultGitHubOptions)
|
||||||
|
|
||||||
|
const opts: OctokitOptions = {
|
||||||
|
log: undefined,
|
||||||
|
userAgent: getUserAgentString(),
|
||||||
|
previews: undefined,
|
||||||
|
retry: retryOpts,
|
||||||
|
request: requestOpts
|
||||||
|
}
|
||||||
|
|
||||||
|
const github = getOctokit(token, opts, retry, requestLog)
|
||||||
|
|
||||||
|
const getArtifactResp = await github.request(
|
||||||
|
'GET /repos/{owner}/{repo}/actions/runs/{run_id}/artifacts{?name}',
|
||||||
|
{
|
||||||
|
owner: repositoryOwner,
|
||||||
|
repo: repositoryName,
|
||||||
|
run_id: workflowRunId,
|
||||||
|
name: artifactName
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (getArtifactResp.status !== 200) {
|
||||||
|
core.warning(`non-200 response from GitHub API: ${getArtifactResp.status}`)
|
||||||
|
return {
|
||||||
|
success: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getArtifactResp.data.artifacts.length === 0) {
|
||||||
|
core.warning('no artifacts found')
|
||||||
|
return {
|
||||||
|
success: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getArtifactResp.data.artifacts.length > 1) {
|
||||||
|
core.warning(
|
||||||
|
'more than one artifact found for a single name, returning first'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
artifact: {
|
||||||
|
name: getArtifactResp.data.artifacts[0].name,
|
||||||
|
id: getArtifactResp.data.artifacts[0].id,
|
||||||
|
url: getArtifactResp.data.artifacts[0].url,
|
||||||
|
size: getArtifactResp.data.artifacts[0].size_in_bytes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
import {info, warning, debug} from '@actions/core'
|
||||||
|
import {getOctokit} from '@actions/github'
|
||||||
|
import {ListArtifactsResponse, Artifact} from '../shared/interfaces'
|
||||||
|
import {getUserAgentString} from '../shared/user-agent'
|
||||||
|
import {getRetryOptions} from './retry-options'
|
||||||
|
import {defaults as defaultGitHubOptions} from '@actions/github/lib/utils'
|
||||||
|
import {requestLog} from '@octokit/plugin-request-log'
|
||||||
|
import {retry} from '@octokit/plugin-retry'
|
||||||
|
import {OctokitOptions} from '@octokit/core/dist-types/types'
|
||||||
|
|
||||||
|
// Limiting to 1000 for perf reasons
|
||||||
|
const maximumArtifactCount = 1000
|
||||||
|
const paginationCount = 100
|
||||||
|
const maxNumberOfPages = maximumArtifactCount / paginationCount
|
||||||
|
|
||||||
|
export async function listArtifacts(
|
||||||
|
workflowRunId: number,
|
||||||
|
repositoryOwner: string,
|
||||||
|
repositoryName: string,
|
||||||
|
token: string
|
||||||
|
): Promise<ListArtifactsResponse> {
|
||||||
|
info(
|
||||||
|
`Fetching artifact list for workflow run ${workflowRunId} in repository ${repositoryOwner}/${repositoryName}`
|
||||||
|
)
|
||||||
|
|
||||||
|
const artifacts: Artifact[] = []
|
||||||
|
const [retryOpts, requestOpts] = getRetryOptions(defaultGitHubOptions)
|
||||||
|
|
||||||
|
const opts: OctokitOptions = {
|
||||||
|
log: undefined,
|
||||||
|
userAgent: getUserAgentString(),
|
||||||
|
previews: undefined,
|
||||||
|
retry: retryOpts,
|
||||||
|
request: requestOpts
|
||||||
|
}
|
||||||
|
|
||||||
|
const github = getOctokit(token, opts, retry, requestLog)
|
||||||
|
|
||||||
|
let currentPageNumber = 1
|
||||||
|
const {data: listArtifactResponse} =
|
||||||
|
await github.rest.actions.listWorkflowRunArtifacts({
|
||||||
|
owner: repositoryOwner,
|
||||||
|
repo: repositoryName,
|
||||||
|
run_id: workflowRunId,
|
||||||
|
per_page: paginationCount,
|
||||||
|
page: currentPageNumber
|
||||||
|
})
|
||||||
|
|
||||||
|
let numberOfPages = Math.ceil(
|
||||||
|
listArtifactResponse.total_count / paginationCount
|
||||||
|
)
|
||||||
|
const totalArtifactCount = listArtifactResponse.total_count
|
||||||
|
if (totalArtifactCount > maximumArtifactCount) {
|
||||||
|
warning(
|
||||||
|
`Workflow run ${workflowRunId} has more than 1000 artifacts. Results will be incomplete as only the first ${maximumArtifactCount} artifacts will be returned`
|
||||||
|
)
|
||||||
|
numberOfPages = maxNumberOfPages
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate over the first page
|
||||||
|
for (const artifact of listArtifactResponse.artifacts) {
|
||||||
|
artifacts.push({
|
||||||
|
name: artifact.name,
|
||||||
|
id: artifact.id,
|
||||||
|
url: artifact.url,
|
||||||
|
size: artifact.size_in_bytes
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate over any remaining pages
|
||||||
|
for (
|
||||||
|
currentPageNumber;
|
||||||
|
currentPageNumber < numberOfPages;
|
||||||
|
currentPageNumber++
|
||||||
|
) {
|
||||||
|
currentPageNumber++
|
||||||
|
debug(`Fetching page ${currentPageNumber} of artifact list`)
|
||||||
|
|
||||||
|
const {data: listArtifactResponse} =
|
||||||
|
await github.rest.actions.listWorkflowRunArtifacts({
|
||||||
|
owner: repositoryOwner,
|
||||||
|
repo: repositoryName,
|
||||||
|
run_id: workflowRunId,
|
||||||
|
per_page: paginationCount,
|
||||||
|
page: currentPageNumber
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const artifact of listArtifactResponse.artifacts) {
|
||||||
|
artifacts.push({
|
||||||
|
name: artifact.name,
|
||||||
|
id: artifact.id,
|
||||||
|
url: artifact.url,
|
||||||
|
size: artifact.size_in_bytes
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info(`Finished fetching artifact list`)
|
||||||
|
|
||||||
|
return {
|
||||||
|
artifacts
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
import * as core from '@actions/core'
|
||||||
|
import {OctokitOptions} from '@octokit/core/dist-types/types'
|
||||||
|
import {RequestRequestOptions} from '@octokit/types'
|
||||||
|
|
||||||
|
export type RetryOptions = {
|
||||||
|
doNotRetry?: number[]
|
||||||
|
enabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defaults for fetching artifacts
|
||||||
|
const defaultMaxRetryNumber = 5
|
||||||
|
const defaultExemptStatusCodes = [400, 401, 403, 404, 422] // https://github.com/octokit/plugin-retry.js/blob/9a2443746c350b3beedec35cf26e197ea318a261/src/index.ts#L14
|
||||||
|
|
||||||
|
export function getRetryOptions(
|
||||||
|
defaultOptions: OctokitOptions,
|
||||||
|
retries: number = defaultMaxRetryNumber,
|
||||||
|
exemptStatusCodes: number[] = defaultExemptStatusCodes
|
||||||
|
): [RetryOptions, RequestRequestOptions | undefined] {
|
||||||
|
if (retries <= 0) {
|
||||||
|
return [{enabled: false}, defaultOptions.request]
|
||||||
|
}
|
||||||
|
|
||||||
|
const retryOptions: RetryOptions = {
|
||||||
|
enabled: true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exemptStatusCodes.length > 0) {
|
||||||
|
retryOptions.doNotRetry = exemptStatusCodes
|
||||||
|
}
|
||||||
|
|
||||||
|
// The GitHub type has some defaults for `options.request`
|
||||||
|
// see: https://github.com/actions/toolkit/blob/4fbc5c941a57249b19562015edbd72add14be93d/packages/github/src/utils.ts#L15
|
||||||
|
// We pass these in here so they are not overridden.
|
||||||
|
const requestOptions: RequestRequestOptions = {
|
||||||
|
...defaultOptions.request,
|
||||||
|
retries
|
||||||
|
}
|
||||||
|
|
||||||
|
core.debug(
|
||||||
|
`GitHub client configured with: (retries: ${
|
||||||
|
requestOptions.retries
|
||||||
|
}, retry-exempt-status-code: ${
|
||||||
|
retryOptions.doNotRetry ?? 'octokit default: [400, 401, 403, 404, 422]'
|
||||||
|
})`
|
||||||
|
)
|
||||||
|
|
||||||
|
return [retryOptions, requestOptions]
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import {HttpClient, HttpClientResponse, HttpCodes} from '@actions/http-client'
|
import {HttpClient, HttpClientResponse, HttpCodes} from '@actions/http-client'
|
||||||
import {BearerCredentialHandler} from '@actions/http-client/lib/auth'
|
import {BearerCredentialHandler} from '@actions/http-client/lib/auth'
|
||||||
import {info} from '@actions/core'
|
import {info, debug} from '@actions/core'
|
||||||
import {ArtifactServiceClientJSON} from '../../generated'
|
import {ArtifactServiceClientJSON} from '../../generated'
|
||||||
import {getResultsServiceUrl, getRuntimeToken} from './config'
|
import {getResultsServiceUrl, getRuntimeToken} from './config'
|
||||||
|
|
||||||
|
@ -53,11 +53,10 @@ class ArtifactHttpClient implements Rpc {
|
||||||
data: object | Uint8Array
|
data: object | Uint8Array
|
||||||
): Promise<object | Uint8Array> {
|
): Promise<object | Uint8Array> {
|
||||||
const url = `${this.baseUrl}/twirp/${service}/${method}`
|
const url = `${this.baseUrl}/twirp/${service}/${method}`
|
||||||
|
debug(`Requesting ${url}`)
|
||||||
const headers = {
|
const headers = {
|
||||||
'Content-Type': contentType
|
'Content-Type': contentType
|
||||||
}
|
}
|
||||||
info(`Making request to ${url} with data: ${JSON.stringify(data)}`)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await this.retryableRequest(async () =>
|
const response = await this.retryableRequest(async () =>
|
||||||
this.httpClient.post(url, JSON.stringify(data), headers)
|
this.httpClient.post(url, JSON.stringify(data), headers)
|
||||||
|
@ -65,7 +64,7 @@ class ArtifactHttpClient implements Rpc {
|
||||||
const body = await response.readBody()
|
const body = await response.readBody()
|
||||||
return JSON.parse(body)
|
return JSON.parse(body)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(error.message)
|
throw new Error(`Failed to ${method}: ${error.message}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,9 @@
|
||||||
|
// Used for controlling the highWaterMark value of the zip that is being streamed
|
||||||
|
// The same value is used as the chunk size that is use during upload to blob storage
|
||||||
|
export function getUploadChunkSize(): number {
|
||||||
|
return 8 * 1024 * 1024 // 8 MB Chunks
|
||||||
|
}
|
||||||
|
|
||||||
export function getRuntimeToken(): string {
|
export function getRuntimeToken(): string {
|
||||||
const token = process.env['ACTIONS_RUNTIME_TOKEN']
|
const token = process.env['ACTIONS_RUNTIME_TOKEN']
|
||||||
if (!token) {
|
if (!token) {
|
||||||
|
@ -13,3 +19,18 @@ export function getResultsServiceUrl(): string {
|
||||||
}
|
}
|
||||||
return resultsUrl
|
return resultsUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isGhes(): boolean {
|
||||||
|
const ghUrl = new URL(
|
||||||
|
process.env['GITHUB_SERVER_URL'] || 'https://github.com'
|
||||||
|
)
|
||||||
|
return ghUrl.hostname.toUpperCase() !== 'GITHUB.COM'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getGitHubWorkspaceDir(): string {
|
||||||
|
const ghWorkspaceDir = process.env['GITHUB_WORKSPACE']
|
||||||
|
if (!ghWorkspaceDir) {
|
||||||
|
throw new Error('Unable to get the GITHUB_WORKSPACE env variable')
|
||||||
|
}
|
||||||
|
return ghWorkspaceDir
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,121 @@
|
||||||
|
/*****************************************************************************
|
||||||
|
* *
|
||||||
|
* UploadArtifact *
|
||||||
|
* *
|
||||||
|
*****************************************************************************/
|
||||||
|
export interface UploadResponse {
|
||||||
|
/**
|
||||||
|
* Denotes if an artifact was successfully uploaded
|
||||||
|
*/
|
||||||
|
success: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Total size of the artifact in bytes. Not provided if no artifact was uploaded
|
||||||
|
*/
|
||||||
|
size?: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id of the artifact that was created. Not provided if no artifact was uploaded
|
||||||
|
* This ID can be used as input to other APIs to download, delete or get more information about an artifact: https://docs.github.com/en/rest/actions/artifacts
|
||||||
|
*/
|
||||||
|
id?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UploadOptions {
|
||||||
|
/**
|
||||||
|
* Duration after which artifact will expire in days.
|
||||||
|
*
|
||||||
|
* By default artifact expires after 90 days:
|
||||||
|
* https://docs.github.com/en/actions/configuring-and-managing-workflows/persisting-workflow-data-using-artifacts#downloading-and-deleting-artifacts-after-a-workflow-run-is-complete
|
||||||
|
*
|
||||||
|
* Use this option to override the default expiry.
|
||||||
|
*
|
||||||
|
* Min value: 1
|
||||||
|
* Max value: 90 unless changed by repository setting
|
||||||
|
*
|
||||||
|
* If this is set to a greater value than the retention settings allowed, the retention on artifacts
|
||||||
|
* will be reduced to match the max value allowed on server, and the upload process will continue. An
|
||||||
|
* input of 0 assumes default retention setting.
|
||||||
|
*/
|
||||||
|
retentionDays?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/*****************************************************************************
|
||||||
|
* *
|
||||||
|
* GetArtifact *
|
||||||
|
* *
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
export interface GetArtifactResponse {
|
||||||
|
/**
|
||||||
|
* If an artifact was found
|
||||||
|
*/
|
||||||
|
success: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metadata about the artifact that was found
|
||||||
|
*/
|
||||||
|
artifact?: Artifact
|
||||||
|
}
|
||||||
|
|
||||||
|
/*****************************************************************************
|
||||||
|
* *
|
||||||
|
* ListArtifact *
|
||||||
|
* *
|
||||||
|
*****************************************************************************/
|
||||||
|
export interface ListArtifactsResponse {
|
||||||
|
/**
|
||||||
|
* A list of artifacts that were found
|
||||||
|
*/
|
||||||
|
artifacts: Artifact[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/*****************************************************************************
|
||||||
|
* *
|
||||||
|
* DownloadArtifact *
|
||||||
|
* *
|
||||||
|
*****************************************************************************/
|
||||||
|
export interface DownloadArtifactResponse {
|
||||||
|
/**
|
||||||
|
* If the artifact download was successful
|
||||||
|
*/
|
||||||
|
success: boolean
|
||||||
|
/**
|
||||||
|
* The path where the artifact was downloaded to
|
||||||
|
*/
|
||||||
|
downloadPath?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DownloadArtifactOptions {
|
||||||
|
/**
|
||||||
|
* Denotes where the artifact will be downloaded to. If not specified then the artifact is download to GITHUB_WORKSPACE
|
||||||
|
*/
|
||||||
|
path?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/*****************************************************************************
|
||||||
|
* *
|
||||||
|
* Shared *
|
||||||
|
* *
|
||||||
|
*****************************************************************************/
|
||||||
|
export interface Artifact {
|
||||||
|
/**
|
||||||
|
* The name of the artifact
|
||||||
|
*/
|
||||||
|
name: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ID of the artifact
|
||||||
|
*/
|
||||||
|
id: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The URL of the artifact
|
||||||
|
*/
|
||||||
|
url: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The size of the artifact in bytes
|
||||||
|
*/
|
||||||
|
size: number
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
|
||||||
|
const packageJson = require('../../../package.json')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure that this User Agent String is used in all HTTP calls so that we can monitor telemetry between different versions of this package
|
||||||
|
*/
|
||||||
|
export function getUserAgentString(): string {
|
||||||
|
return `@actions/artifact-${packageJson.version}`
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
import {getRuntimeToken} from './config'
|
||||||
|
import jwt_decode from 'jwt-decode'
|
||||||
|
|
||||||
|
export interface BackendIds {
|
||||||
|
workflowRunBackendId: string
|
||||||
|
workflowJobRunBackendId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ActionsToken {
|
||||||
|
scp: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const InvalidJwtError = new Error(
|
||||||
|
'Failed to get backend IDs: The provided JWT token is invalid'
|
||||||
|
)
|
||||||
|
|
||||||
|
// uses the JWT token claims to get the
|
||||||
|
// workflow run and workflow job run backend ids
|
||||||
|
export function getBackendIdsFromToken(): BackendIds {
|
||||||
|
const token = getRuntimeToken()
|
||||||
|
const decoded = jwt_decode<ActionsToken>(token)
|
||||||
|
if (!decoded.scp) {
|
||||||
|
throw InvalidJwtError
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* example decoded:
|
||||||
|
* {
|
||||||
|
* scp: "Actions.ExampleScope Actions.Results:ce7f54c7-61c7-4aae-887f-30da475f5f1a:ca395085-040a-526b-2ce8-bdc85f692774"
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
const scpParts = decoded.scp.split(' ')
|
||||||
|
if (scpParts.length === 0) {
|
||||||
|
throw InvalidJwtError
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* example scpParts:
|
||||||
|
* ["Actions.ExampleScope", "Actions.Results:ce7f54c7-61c7-4aae-887f-30da475f5f1a:ca395085-040a-526b-2ce8-bdc85f692774"]
|
||||||
|
*/
|
||||||
|
|
||||||
|
for (const scopes of scpParts) {
|
||||||
|
const scopeParts = scopes.split(':')
|
||||||
|
/*
|
||||||
|
* example scopeParts:
|
||||||
|
* ["Actions.Results", "ce7f54c7-61c7-4aae-887f-30da475f5f1a", "ca395085-040a-526b-2ce8-bdc85f692774"]
|
||||||
|
*/
|
||||||
|
if (scopeParts.length !== 3) {
|
||||||
|
// not the Actions.Results scope
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scopeParts[0] !== 'Actions.Results') {
|
||||||
|
// not the Actions.Results scope
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
workflowRunBackendId: scopeParts[1],
|
||||||
|
workflowJobRunBackendId: scopeParts[2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw InvalidJwtError
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
import {BlobClient, BlockBlobUploadStreamOptions} from '@azure/storage-blob'
|
||||||
|
import {TransferProgressEvent} from '@azure/core-http'
|
||||||
|
import {ZipUploadStream} from './zip'
|
||||||
|
import {getUploadChunkSize} from '../shared/config'
|
||||||
|
import * as core from '@actions/core'
|
||||||
|
import * as crypto from 'crypto'
|
||||||
|
import * as stream from 'stream'
|
||||||
|
|
||||||
|
export interface BlobUploadResponse {
|
||||||
|
/**
|
||||||
|
* If the upload was successful or not
|
||||||
|
*/
|
||||||
|
isSuccess: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The total reported upload size in bytes. Empty if the upload failed
|
||||||
|
*/
|
||||||
|
uploadSize?: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The MD5 hash of the uploaded file. Empty if the upload failed
|
||||||
|
*/
|
||||||
|
md5Hash?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function uploadZipToBlobStorage(
|
||||||
|
authenticatedUploadURL: string,
|
||||||
|
zipUploadStream: ZipUploadStream
|
||||||
|
): Promise<BlobUploadResponse> {
|
||||||
|
let uploadByteCount = 0
|
||||||
|
|
||||||
|
const maxBuffers = 5
|
||||||
|
const bufferSize = getUploadChunkSize()
|
||||||
|
const blobClient = new BlobClient(authenticatedUploadURL)
|
||||||
|
const blockBlobClient = blobClient.getBlockBlobClient()
|
||||||
|
|
||||||
|
core.debug(
|
||||||
|
`Uploading artifact zip to blob storage with maxBuffers: ${maxBuffers}, bufferSize: ${bufferSize}`
|
||||||
|
)
|
||||||
|
|
||||||
|
const uploadCallback = (progress: TransferProgressEvent): void => {
|
||||||
|
core.info(`Uploaded bytes ${progress.loadedBytes}`)
|
||||||
|
uploadByteCount = progress.loadedBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: BlockBlobUploadStreamOptions = {
|
||||||
|
blobHTTPHeaders: {blobContentType: 'zip'},
|
||||||
|
onProgress: uploadCallback
|
||||||
|
}
|
||||||
|
|
||||||
|
let md5Hash: string | undefined = undefined
|
||||||
|
const uploadStream = new stream.PassThrough()
|
||||||
|
const hashStream = crypto.createHash('md5')
|
||||||
|
|
||||||
|
zipUploadStream.pipe(uploadStream) // This stream is used for the upload
|
||||||
|
zipUploadStream.pipe(hashStream).setEncoding('hex') // This stream is used to compute a hash of the zip content that gets used. Integrity check
|
||||||
|
|
||||||
|
try {
|
||||||
|
core.info('Beginning upload of artifact content to blob storage')
|
||||||
|
|
||||||
|
await blockBlobClient.uploadStream(
|
||||||
|
uploadStream,
|
||||||
|
bufferSize,
|
||||||
|
maxBuffers,
|
||||||
|
options
|
||||||
|
)
|
||||||
|
|
||||||
|
core.info('Finished uploading artifact content to blob storage!')
|
||||||
|
|
||||||
|
hashStream.end()
|
||||||
|
md5Hash = hashStream.read() as string
|
||||||
|
core.info(`MD5 hash of uploaded artifact zip is ${md5Hash}`)
|
||||||
|
} catch (error) {
|
||||||
|
core.warning(
|
||||||
|
`Failed to upload artifact zip to blob storage, error: ${error}`
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
isSuccess: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uploadByteCount === 0) {
|
||||||
|
core.warning(
|
||||||
|
`No data was uploaded to blob storage. Reported upload byte count is 0`
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
isSuccess: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isSuccess: true,
|
||||||
|
uploadSize: uploadByteCount,
|
||||||
|
md5Hash
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
import {Timestamp} from '../../generated'
|
||||||
|
import * as core from '@actions/core'
|
||||||
|
|
||||||
|
export function getExpiration(retentionDays?: number): Timestamp | undefined {
|
||||||
|
if (!retentionDays) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxRetentionDays = getRetentionDays()
|
||||||
|
if (maxRetentionDays && maxRetentionDays < retentionDays) {
|
||||||
|
core.warning(
|
||||||
|
`Retention days cannot be greater than the maximum allowed retention set within the repository. Using ${maxRetentionDays} instead.`
|
||||||
|
)
|
||||||
|
retentionDays = maxRetentionDays
|
||||||
|
}
|
||||||
|
|
||||||
|
const expirationDate = new Date()
|
||||||
|
expirationDate.setDate(expirationDate.getDate() + retentionDays)
|
||||||
|
|
||||||
|
return Timestamp.fromDate(expirationDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRetentionDays(): number | undefined {
|
||||||
|
const retentionDays = process.env['GITHUB_RETENTION_DAYS']
|
||||||
|
if (!retentionDays) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
const days = parseInt(retentionDays)
|
||||||
|
if (isNaN(days)) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return days
|
||||||
|
}
|
|
@ -1,18 +1,27 @@
|
||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import {UploadOptions} from './upload-options'
|
import {UploadOptions, UploadResponse} from '../shared/interfaces'
|
||||||
import {UploadResponse} from './upload-response'
|
import {getExpiration} from './retention'
|
||||||
import {validateArtifactName} from './path-and-artifact-name-validation'
|
import {validateArtifactName} from './path-and-artifact-name-validation'
|
||||||
|
import {createArtifactTwirpClient} from '../shared/artifact-twirp-client'
|
||||||
import {
|
import {
|
||||||
UploadZipSpecification,
|
UploadZipSpecification,
|
||||||
getUploadZipSpecification,
|
getUploadZipSpecification,
|
||||||
validateRootDirectory
|
validateRootDirectory
|
||||||
} from './upload-zip-specification'
|
} from './upload-zip-specification'
|
||||||
|
import {getBackendIdsFromToken} from '../shared/util'
|
||||||
|
import {uploadZipToBlobStorage} from './blob-upload'
|
||||||
|
import {createZipUploadStream} from './zip'
|
||||||
|
import {
|
||||||
|
CreateArtifactRequest,
|
||||||
|
FinalizeArtifactRequest,
|
||||||
|
StringValue
|
||||||
|
} from '../../generated'
|
||||||
|
|
||||||
export async function uploadArtifact(
|
export async function uploadArtifact(
|
||||||
name: string,
|
name: string,
|
||||||
files: string[],
|
files: string[],
|
||||||
rootDirectory: string,
|
rootDirectory: string,
|
||||||
options?: UploadOptions | undefined // eslint-disable-line @typescript-eslint/no-unused-vars
|
options?: UploadOptions | undefined
|
||||||
): Promise<UploadResponse> {
|
): Promise<UploadResponse> {
|
||||||
validateArtifactName(name)
|
validateArtifactName(name)
|
||||||
validateRootDirectory(rootDirectory)
|
validateRootDirectory(rootDirectory)
|
||||||
|
@ -28,13 +37,93 @@ export async function uploadArtifact(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO - Implement upload functionality
|
const zipUploadStream = await createZipUploadStream(zipSpecification)
|
||||||
|
|
||||||
const uploadResponse: UploadResponse = {
|
// get the IDs needed for the artifact creation
|
||||||
success: true,
|
const backendIds = getBackendIdsFromToken()
|
||||||
size: 0,
|
if (!backendIds.workflowRunBackendId || !backendIds.workflowJobRunBackendId) {
|
||||||
id: 0
|
core.warning(
|
||||||
|
`Failed to get the necessary backend ids which are required to create the artifact`
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
success: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
core.debug(`Workflow Run Backend ID: ${backendIds.workflowRunBackendId}`)
|
||||||
|
core.debug(
|
||||||
|
`Workflow Job Run Backend ID: ${backendIds.workflowJobRunBackendId}`
|
||||||
|
)
|
||||||
|
|
||||||
|
// create the artifact client
|
||||||
|
const artifactClient = createArtifactTwirpClient('upload')
|
||||||
|
|
||||||
|
// create the artifact
|
||||||
|
const createArtifactReq: CreateArtifactRequest = {
|
||||||
|
workflowRunBackendId: backendIds.workflowRunBackendId,
|
||||||
|
workflowJobRunBackendId: backendIds.workflowJobRunBackendId,
|
||||||
|
name,
|
||||||
|
version: 4
|
||||||
}
|
}
|
||||||
|
|
||||||
return uploadResponse
|
// if there is a retention period, add it to the request
|
||||||
|
const expiresAt = getExpiration(options?.retentionDays)
|
||||||
|
if (expiresAt) {
|
||||||
|
createArtifactReq.expiresAt = expiresAt
|
||||||
|
}
|
||||||
|
|
||||||
|
const createArtifactResp =
|
||||||
|
await artifactClient.CreateArtifact(createArtifactReq)
|
||||||
|
if (!createArtifactResp.ok) {
|
||||||
|
core.warning(`Failed to create artifact`)
|
||||||
|
return {
|
||||||
|
success: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload zip to blob storage
|
||||||
|
const uploadResult = await uploadZipToBlobStorage(
|
||||||
|
createArtifactResp.signedUploadUrl,
|
||||||
|
zipUploadStream
|
||||||
|
)
|
||||||
|
if (uploadResult.isSuccess === false) {
|
||||||
|
return {
|
||||||
|
success: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// finalize the artifact
|
||||||
|
const finalizeArtifactReq: FinalizeArtifactRequest = {
|
||||||
|
workflowRunBackendId: backendIds.workflowRunBackendId,
|
||||||
|
workflowJobRunBackendId: backendIds.workflowJobRunBackendId,
|
||||||
|
name,
|
||||||
|
size: uploadResult.uploadSize ? uploadResult.uploadSize.toString() : '0'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uploadResult.md5Hash) {
|
||||||
|
finalizeArtifactReq.hash = StringValue.create({
|
||||||
|
value: `md5:${uploadResult.md5Hash}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
core.info(`Finalizing artifact upload`)
|
||||||
|
|
||||||
|
const finalizeArtifactResp =
|
||||||
|
await artifactClient.FinalizeArtifact(finalizeArtifactReq)
|
||||||
|
if (!finalizeArtifactResp.ok) {
|
||||||
|
core.warning(`Failed to finalize artifact`)
|
||||||
|
return {
|
||||||
|
success: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const artifactId = BigInt(finalizeArtifactResp.artifactId)
|
||||||
|
core.info(
|
||||||
|
`Artifact ${name}.zip successfully finalized. Artifact ID ${artifactId}`
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
size: uploadResult.uploadSize,
|
||||||
|
id: Number(artifactId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
export interface UploadOptions {
|
|
||||||
/**
|
|
||||||
* Duration after which artifact will expire in days.
|
|
||||||
*
|
|
||||||
* By default artifact expires after 90 days:
|
|
||||||
* https://docs.github.com/en/actions/configuring-and-managing-workflows/persisting-workflow-data-using-artifacts#downloading-and-deleting-artifacts-after-a-workflow-run-is-complete
|
|
||||||
*
|
|
||||||
* Use this option to override the default expiry.
|
|
||||||
*
|
|
||||||
* Min value: 1
|
|
||||||
* Max value: 90 unless changed by repository setting
|
|
||||||
*
|
|
||||||
* If this is set to a greater value than the retention settings allowed, the retention on artifacts
|
|
||||||
* will be reduced to match the max value allowed on server, and the upload process will continue. An
|
|
||||||
* input of 0 assumes default retention setting.
|
|
||||||
*/
|
|
||||||
retentionDays?: number
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
export interface UploadResponse {
|
|
||||||
/**
|
|
||||||
* Denotes if an artifact was successfully uploaded
|
|
||||||
*/
|
|
||||||
success: boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Total size of the artifact in bytes. Not provided if no artifact was uploaded
|
|
||||||
*/
|
|
||||||
size?: number
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The id of the artifact that was created. Not provided if no artifact was uploaded
|
|
||||||
* This ID can be used as input to other APIs to download, delete or get more information about an artifact: https://docs.github.com/en/rest/actions/artifacts
|
|
||||||
*/
|
|
||||||
id?: number
|
|
||||||
}
|
|
|
@ -0,0 +1,97 @@
|
||||||
|
import * as stream from 'stream'
|
||||||
|
import * as archiver from 'archiver'
|
||||||
|
import * as core from '@actions/core'
|
||||||
|
import {createReadStream} from 'fs'
|
||||||
|
import {UploadZipSpecification} from './upload-zip-specification'
|
||||||
|
import {getUploadChunkSize} from '../shared/config'
|
||||||
|
|
||||||
|
// Custom stream transformer so we can set the highWaterMark property
|
||||||
|
// See https://github.com/nodejs/node/issues/8855
|
||||||
|
export class ZipUploadStream extends stream.Transform {
|
||||||
|
constructor(bufferSize: number) {
|
||||||
|
super({
|
||||||
|
highWaterMark: bufferSize
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
_transform(chunk: any, enc: any, cb: any): void {
|
||||||
|
cb(null, chunk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createZipUploadStream(
|
||||||
|
uploadSpecification: UploadZipSpecification[]
|
||||||
|
): Promise<ZipUploadStream> {
|
||||||
|
const zip = archiver.create('zip', {
|
||||||
|
zlib: {level: 9} // Sets the compression level.
|
||||||
|
// Available options are 0-9
|
||||||
|
// 0 => no compression
|
||||||
|
// 1 => fastest with low compression
|
||||||
|
// 9 => highest compression ratio but the slowest
|
||||||
|
})
|
||||||
|
|
||||||
|
// register callbacks for various events during the zip lifecycle
|
||||||
|
zip.on('error', zipErrorCallback)
|
||||||
|
zip.on('warning', zipWarningCallback)
|
||||||
|
zip.on('finish', zipFinishCallback)
|
||||||
|
zip.on('end', zipEndCallback)
|
||||||
|
|
||||||
|
for (const file of uploadSpecification) {
|
||||||
|
if (file.sourcePath !== null) {
|
||||||
|
// Add a normal file to the zip
|
||||||
|
zip.append(createReadStream(file.sourcePath), {
|
||||||
|
name: file.destinationPath
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Add a directory to the zip
|
||||||
|
zip.append('', {name: file.destinationPath})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bufferSize = getUploadChunkSize()
|
||||||
|
const zipUploadStream = new ZipUploadStream(bufferSize)
|
||||||
|
|
||||||
|
core.debug(
|
||||||
|
`Zip write high watermark value ${zipUploadStream.writableHighWaterMark}`
|
||||||
|
)
|
||||||
|
core.debug(
|
||||||
|
`Zip read high watermark value ${zipUploadStream.readableHighWaterMark}`
|
||||||
|
)
|
||||||
|
|
||||||
|
zip.pipe(zipUploadStream)
|
||||||
|
zip.finalize()
|
||||||
|
|
||||||
|
return zipUploadStream
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const zipErrorCallback = (error: any): void => {
|
||||||
|
core.error('An error has occurred while creating the zip file for upload')
|
||||||
|
core.info(error)
|
||||||
|
|
||||||
|
throw new Error('An error has occurred during zip creation for the artifact')
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const zipWarningCallback = (error: any): void => {
|
||||||
|
if (error.code === 'ENOENT') {
|
||||||
|
core.warning(
|
||||||
|
'ENOENT warning during artifact zip creation. No such file or directory'
|
||||||
|
)
|
||||||
|
core.info(error)
|
||||||
|
} else {
|
||||||
|
core.warning(
|
||||||
|
`A non-blocking warning has occurred during artifact zip creation: ${error.code}`
|
||||||
|
)
|
||||||
|
core.info(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const zipFinishCallback = (): void => {
|
||||||
|
core.debug('Zip stream for upload has finished.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const zipEndCallback = (): void => {
|
||||||
|
core.debug('Zip stream for upload has ended.')
|
||||||
|
}
|
|
@ -17,13 +17,13 @@
|
||||||
"@azure/abort-controller": "^1.1.0",
|
"@azure/abort-controller": "^1.1.0",
|
||||||
"@azure/ms-rest-js": "^2.6.0",
|
"@azure/ms-rest-js": "^2.6.0",
|
||||||
"@azure/storage-blob": "^12.13.0",
|
"@azure/storage-blob": "^12.13.0",
|
||||||
"semver": "^6.1.0",
|
"semver": "^6.3.1",
|
||||||
"uuid": "^3.3.3"
|
"uuid": "^3.3.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/semver": "^6.0.0",
|
"@types/semver": "^6.0.0",
|
||||||
"@types/uuid": "^3.4.5",
|
"@types/uuid": "^3.4.5",
|
||||||
"typescript": "^4.8.0"
|
"typescript": "^5.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@actions/core": {
|
"node_modules/@actions/core": {
|
||||||
|
@ -474,16 +474,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "4.9.5",
|
"version": "5.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
|
||||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
"integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=4.2.0"
|
"node": ">=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/uuid": {
|
"node_modules/uuid": {
|
||||||
|
@ -895,9 +895,9 @@
|
||||||
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="
|
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="
|
||||||
},
|
},
|
||||||
"typescript": {
|
"typescript": {
|
||||||
"version": "4.9.5",
|
"version": "5.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
|
||||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
"integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"uuid": {
|
"uuid": {
|
||||||
|
|
|
@ -45,12 +45,12 @@
|
||||||
"@azure/abort-controller": "^1.1.0",
|
"@azure/abort-controller": "^1.1.0",
|
||||||
"@azure/ms-rest-js": "^2.6.0",
|
"@azure/ms-rest-js": "^2.6.0",
|
||||||
"@azure/storage-blob": "^12.13.0",
|
"@azure/storage-blob": "^12.13.0",
|
||||||
"semver": "^6.1.0",
|
"semver": "^6.3.1",
|
||||||
"uuid": "^3.3.3"
|
"uuid": "^3.3.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/semver": "^6.0.0",
|
"@types/semver": "^6.0.0",
|
||||||
"@types/uuid": "^3.4.5",
|
"@types/uuid": "^3.4.5",
|
||||||
"typescript": "^4.8.0"
|
"typescript": "^5.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
# @actions/core Releases
|
# @actions/core Releases
|
||||||
|
|
||||||
|
### 1.10.1
|
||||||
|
- Fix error message reference in oidc utils [#1511](https://github.com/actions/toolkit/pull/1511)
|
||||||
|
|
||||||
### 1.10.0
|
### 1.10.0
|
||||||
- `saveState` and `setOutput` now use environment files if available [#1178](https://github.com/actions/toolkit/pull/1178)
|
- `saveState` and `setOutput` now use environment files if available [#1178](https://github.com/actions/toolkit/pull/1178)
|
||||||
- `getMultilineInput` now correctly trims whitespace by default [#1185](https://github.com/actions/toolkit/pull/1185)
|
- `getMultilineInput` now correctly trims whitespace by default [#1185](https://github.com/actions/toolkit/pull/1185)
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@actions/core",
|
"name": "@actions/core",
|
||||||
"version": "1.10.0",
|
"version": "1.10.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/http-client": "^2.0.1",
|
"@actions/http-client": "^2.0.1",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@actions/core",
|
"name": "@actions/core",
|
||||||
"version": "1.10.0",
|
"version": "1.10.1",
|
||||||
"description": "Actions core lib",
|
"description": "Actions core lib",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"github",
|
"github",
|
||||||
|
|
|
@ -52,7 +52,7 @@ export class OidcClient {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to get ID Token. \n
|
`Failed to get ID Token. \n
|
||||||
Error Code : ${error.statusCode}\n
|
Error Code : ${error.statusCode}\n
|
||||||
Error Message: ${error.result.message}`
|
Error Message: ${error.message}`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -631,7 +631,7 @@ class ExecState extends events.EventEmitter {
|
||||||
private delay = 10000 // 10 seconds
|
private delay = 10000 // 10 seconds
|
||||||
private done = false
|
private done = false
|
||||||
private options: im.ExecOptions
|
private options: im.ExecOptions
|
||||||
private timeout: NodeJS.Timer | null = null
|
private timeout: NodeJS.Timeout | null = null
|
||||||
private toolPath: string
|
private toolPath: string
|
||||||
|
|
||||||
CheckComplete(): void {
|
CheckComplete(): void {
|
||||||
|
|
|
@ -17,7 +17,7 @@ describe('@actions/github', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
// Start proxy server
|
// Start proxy server
|
||||||
proxyServer = proxy()
|
proxyServer = proxy()
|
||||||
await new Promise(resolve => {
|
await new Promise<void>(resolve => {
|
||||||
const port = Number(proxyUrl.split(':')[2])
|
const port = Number(proxyUrl.split(':')[2])
|
||||||
proxyServer.listen(port, () => resolve())
|
proxyServer.listen(port, () => resolve())
|
||||||
})
|
})
|
||||||
|
@ -32,7 +32,7 @@ describe('@actions/github', () => {
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
// Stop proxy server
|
// Stop proxy server
|
||||||
await new Promise(resolve => {
|
await new Promise<void>(resolve => {
|
||||||
proxyServer.once('close', () => resolve())
|
proxyServer.once('close', () => resolve())
|
||||||
proxyServer.close()
|
proxyServer.close()
|
||||||
})
|
})
|
||||||
|
|
|
@ -13,9 +13,9 @@ describe('@actions/github', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
// Start proxy server
|
// Start proxy server
|
||||||
proxyServer = proxy()
|
proxyServer = proxy()
|
||||||
await new Promise(resolve => {
|
await new Promise<void>(resolve => {
|
||||||
const port = Number(proxyUrl.split(':')[2])
|
const port = Number(proxyUrl.split(':')[2])
|
||||||
proxyServer.listen(port, () => resolve(null))
|
proxyServer.listen(port, () => resolve())
|
||||||
})
|
})
|
||||||
proxyServer.on('connect', req => {
|
proxyServer.on('connect', req => {
|
||||||
proxyConnects.push(req.url ?? '')
|
proxyConnects.push(req.url ?? '')
|
||||||
|
@ -29,8 +29,8 @@ describe('@actions/github', () => {
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
// Stop proxy server
|
// Stop proxy server
|
||||||
await new Promise(resolve => {
|
await new Promise<void>(resolve => {
|
||||||
proxyServer.once('close', () => resolve(null))
|
proxyServer.once('close', () => resolve())
|
||||||
proxyServer.close()
|
proxyServer.close()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -167,8 +167,12 @@ function normalizeSeparators(p: string): string {
|
||||||
function isUnixExecutable(stats: fs.Stats): boolean {
|
function isUnixExecutable(stats: fs.Stats): boolean {
|
||||||
return (
|
return (
|
||||||
(stats.mode & 1) > 0 ||
|
(stats.mode & 1) > 0 ||
|
||||||
((stats.mode & 8) > 0 && stats.gid === process.getgid()) ||
|
((stats.mode & 8) > 0 &&
|
||||||
((stats.mode & 64) > 0 && stats.uid === process.getuid())
|
process.getgid !== undefined &&
|
||||||
|
stats.gid === process.getgid()) ||
|
||||||
|
((stats.mode & 64) > 0 &&
|
||||||
|
process.getuid !== undefined &&
|
||||||
|
stats.uid === process.getuid())
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,8 @@
|
||||||
"@actions/http-client": [
|
"@actions/http-client": [
|
||||||
"packages/http-client"
|
"packages/http-client"
|
||||||
],
|
],
|
||||||
}
|
},
|
||||||
|
"useUnknownInCatchVariables": false
|
||||||
},
|
},
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"node_modules",
|
"node_modules",
|
||||||
|
|
Loading…
Reference in New Issue