Merge pull request #53 from ddivad195/main
Implement new functionality, add testing and update documentation
This commit is contained in:
@@ -8,6 +8,5 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checking out!
|
- name: Checking out!
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Publish action package
|
- name: Publish action package
|
||||||
uses: ./
|
uses: ./
|
||||||
|
|||||||
-197
@@ -1,197 +0,0 @@
|
|||||||
# Initial Setup
|
|
||||||
|
|
||||||
After you've cloned the repository to your local machine or codespace, you'll
|
|
||||||
need to perform some initial setup steps before you can develop your action.
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
>
|
|
||||||
> You'll need to have a reasonably modern version of
|
|
||||||
> [Node.js](https://nodejs.org) handy (20.x or later should work!). If you are
|
|
||||||
> using a version manager like [`nodenv`](https://github.com/nodenv/nodenv) or
|
|
||||||
> [`nvm`](https://github.com/nvm-sh/nvm), this template has a `.node-version`
|
|
||||||
> file at the root of the repository that will be used to automatically switch
|
|
||||||
> to the correct version when you `cd` into the repository. Additionally, this
|
|
||||||
> `.node-version` file is used by GitHub Actions in any `actions/setup-node`
|
|
||||||
> actions.
|
|
||||||
|
|
||||||
1. :hammer_and_wrench: Install the dependencies
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
1. :building_construction: Package the TypeScript for distribution
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run bundle
|
|
||||||
```
|
|
||||||
|
|
||||||
1. :white_check_mark: Run the tests
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ npm test
|
|
||||||
|
|
||||||
PASS ./index.test.js
|
|
||||||
✓ throws invalid number (3ms)
|
|
||||||
✓ wait 500 ms (504ms)
|
|
||||||
✓ test runs (95ms)
|
|
||||||
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
## Update the Action Metadata
|
|
||||||
|
|
||||||
The [`action.yml`](action.yml) file defines metadata about your action, such as
|
|
||||||
input(s) and output(s). For details about this file, see
|
|
||||||
[Metadata syntax for GitHub Actions](https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions).
|
|
||||||
|
|
||||||
When you copy this repository, update `action.yml` with the name, description,
|
|
||||||
inputs, and outputs for your action.
|
|
||||||
|
|
||||||
## Update the Action Code
|
|
||||||
|
|
||||||
The [`src/`](./src/) directory is the heart of your action! This contains the
|
|
||||||
source code that will be run when your action is invoked. You can replace the
|
|
||||||
contents of this directory with your own code.
|
|
||||||
|
|
||||||
There are a few things to keep in mind when writing your action code:
|
|
||||||
|
|
||||||
- Most GitHub Actions toolkit and CI/CD operations are processed asynchronously.
|
|
||||||
In `main.ts`, you will see that the action is run in an `async` function.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import * as core from '@actions/core'
|
|
||||||
//...
|
|
||||||
|
|
||||||
async function run() {
|
|
||||||
try {
|
|
||||||
//...
|
|
||||||
} catch (error) {
|
|
||||||
core.setFailed(error.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
For more information about the GitHub Actions toolkit, see the
|
|
||||||
[documentation](https://github.com/actions/toolkit/blob/master/README.md).
|
|
||||||
|
|
||||||
So, what are you waiting for? Go ahead and start customizing your action!
|
|
||||||
|
|
||||||
1. Create a new branch
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git checkout -b releases/v1
|
|
||||||
```
|
|
||||||
|
|
||||||
1. Replace the contents of `src/` with your action code
|
|
||||||
1. Add tests to `__tests__/` for your source code
|
|
||||||
1. Format, test, and build the action
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run all
|
|
||||||
```
|
|
||||||
|
|
||||||
> [!WARNING]
|
|
||||||
>
|
|
||||||
> This step is important! It will run [`ncc`](https://github.com/vercel/ncc)
|
|
||||||
> to build the final JavaScript action code with all dependencies included.
|
|
||||||
> If you do not run this step, your action will not work correctly when it is
|
|
||||||
> used in a workflow. This step also includes the `--license` option for
|
|
||||||
> `ncc`, which will create a license file for all of the production node
|
|
||||||
> modules used in your project.
|
|
||||||
|
|
||||||
1. Commit your changes
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add .
|
|
||||||
git commit -m "My first action is ready!"
|
|
||||||
```
|
|
||||||
|
|
||||||
1. Push them to your repository
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git push -u origin releases/v1
|
|
||||||
```
|
|
||||||
|
|
||||||
1. Create a pull request and get feedback on your action
|
|
||||||
1. Merge the pull request into the `main` branch
|
|
||||||
|
|
||||||
Your action is now published! :rocket:
|
|
||||||
|
|
||||||
For information about versioning your action, see
|
|
||||||
[Versioning](https://github.com/actions/toolkit/blob/master/docs/action-versioning.md)
|
|
||||||
in the GitHub Actions toolkit.
|
|
||||||
|
|
||||||
## Validate the Action
|
|
||||||
|
|
||||||
You can now validate the action by referencing it in a workflow file. For
|
|
||||||
example, [`ci.yml`](./.github/workflows/ci.yml) demonstrates how to reference an
|
|
||||||
action in the same repository.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
id: checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Test Local Action
|
|
||||||
id: test-action
|
|
||||||
uses: ./
|
|
||||||
with:
|
|
||||||
milliseconds: 1000
|
|
||||||
|
|
||||||
- name: Print Output
|
|
||||||
id: output
|
|
||||||
run: echo "${{ steps.test-action.outputs.time }}"
|
|
||||||
```
|
|
||||||
|
|
||||||
For example workflow runs, check out the
|
|
||||||
[Actions tab](https://github.com/actions/typescript-action/actions)! :rocket:
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
After testing, you can create version tag(s) that developers can use to
|
|
||||||
reference different stable versions of your action. For more information, see
|
|
||||||
[Versioning](https://github.com/actions/toolkit/blob/master/docs/action-versioning.md)
|
|
||||||
in the GitHub Actions toolkit.
|
|
||||||
|
|
||||||
To include the action in a workflow in another repository, you can use the
|
|
||||||
`uses` syntax with the `@` symbol to reference a specific branch, tag, or commit
|
|
||||||
hash.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
id: checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Test Local Action
|
|
||||||
id: test-action
|
|
||||||
uses: actions/typescript-action@v1 # Commit with the `v1` tag
|
|
||||||
with:
|
|
||||||
milliseconds: 1000
|
|
||||||
|
|
||||||
- name: Print Output
|
|
||||||
id: output
|
|
||||||
run: echo "${{ steps.test-action.outputs.time }}"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Publishing a new release
|
|
||||||
|
|
||||||
This project includes a helper script designed to streamline the process of
|
|
||||||
tagging and pushing new releases for GitHub Actions.
|
|
||||||
|
|
||||||
GitHub Actions allows users to select a specific version of the action to use,
|
|
||||||
based on release tags. Our script simplifies this process by performing the
|
|
||||||
following steps:
|
|
||||||
|
|
||||||
1. **Retrieving the latest release tag:** The script starts by fetching the most
|
|
||||||
recent release tag by looking at the local data available in your repository.
|
|
||||||
1. **Prompting for a new release tag:** The user is then prompted to enter a new
|
|
||||||
release tag. To assist with this, the script displays the latest release tag
|
|
||||||
and provides a regular expression to validate the format of the new tag.
|
|
||||||
1. **Tagging the new release:** Once a valid new tag is entered, the script tags
|
|
||||||
the new release.
|
|
||||||
1. **Pushing the new tag to the remote:** Finally, the script pushes the new tag
|
|
||||||
to the remote repository. From here, you will need to create a new release in
|
|
||||||
GitHub and users can easily reference the new tag in their workflows.
|
|
||||||
@@ -1,13 +1,10 @@
|
|||||||
# Publish Action Package
|
# Publish Action Package
|
||||||
|
|
||||||
This action packages your action repository as OCI artifacts and publishes it to [GHCR](ghcr.io).
|
_This action_ packages _your action_ as OCI artifacts and publishes it to the [GitHub Container registry](ghcr.io).
|
||||||
|
|
||||||
This allows your action to be consumed as an immutable package to make the actions ecosystem more secure.
|
This allows your action to be consumed as an _immutable_ package even if a [semver](https://semver.org/) is specified in the consumer's workflow file.
|
||||||
|
|
||||||
The whole action repository is packaged by default. Set `path` input to specify which path you want to package.
|
Your action workflow must be triggered on `release` as in the following example. The release's title must follow [semantic versioning](https://semver.org/). Then consumers of your action will then be able to specify the version, e.g., `- uses: your-name/[email protected]` or even `- uses: your-name/your-action@v1`.
|
||||||
|
|
||||||
Make sure you use the [Starter Workflow](https://github.com/actions-on-packages/.github) (TODO) to run the action.
|
|
||||||
Please also ensure you have the release trigger in the workflow where you use this action.
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -16,19 +13,8 @@ Please also ensure you have the release trigger in the workflow where you use th
|
|||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
|
|
||||||
- uses: immutable-actions/publish-action-package@1.0.1
|
- uses: immutable-actions/publish-action-package@v1
|
||||||
with:
|
with:
|
||||||
|
|
||||||
# Personal access token (PAT) or GITHUB_TOKEN with write:package scope used to upload the package to GHCR. The GITHUB_TOKEN is taken by default.
|
|
||||||
#
|
|
||||||
# We recommend using a service account with the least permissions necessary. Also
|
|
||||||
# when generating a new PAT, select the least scopes necessary.
|
|
||||||
#
|
|
||||||
# [Learn more about creating and using encrypted secrets](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets)
|
|
||||||
#
|
|
||||||
# Default: ${{ github.token }}
|
|
||||||
token: ''
|
|
||||||
|
|
||||||
# Relative path of the working directory of the repository to be tar archived
|
# Relative path of the working directory of the repository to be tar archived
|
||||||
# and uploaded as OCI Artifact layer. You can mention multiple files/folders
|
# and uploaded as OCI Artifact layer. You can mention multiple files/folders
|
||||||
# by mentioning relative paths as space separated values.
|
# by mentioning relative paths as space separated values.
|
||||||
@@ -36,38 +22,9 @@ on:
|
|||||||
# This defaults to the entire action repository contents if not explicitly defined.
|
# This defaults to the entire action repository contents if not explicitly defined.
|
||||||
# Default: '.'
|
# Default: '.'
|
||||||
path: 'src/ action.yml dist/'
|
path: 'src/ action.yml dist/'
|
||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
<!-- end usage -->
|
<!-- end usage -->
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
The scripts and documentation in this project are released under the [MIT License](LICENSE)
|
The scripts and documentation in this project are released under the [MIT License](LICENSE).
|
||||||
|
|
||||||
## [Internal] Differences from previous implementation
|
|
||||||
|
|
||||||
This is a new implementation of an Action which publishes a given release to ghcr.io (GitHub Packages).
|
|
||||||
|
|
||||||
It will eventually be moved to our `actions`` org.
|
|
||||||
|
|
||||||
The key differences are:
|
|
||||||
|
|
||||||
- This Action goes directly to GitHub Packages rather than using an API endpoint to pass a bundle to.
|
|
||||||
- This Action uses Node.js libraries to create both a `zip` and `tar.gz` of the content as layers.
|
|
||||||
- This Action creates and publishes the OCI manifest which houses those archives, which was previously done on the backend.
|
|
||||||
- This Action has the goal of generating provenance attestations for any release that is created.
|
|
||||||
- This Action parses and validates that the release tag which triggered it is in a valid SemVer format, either `1.0.3-prerelease` or `v1.0.0-prerelease`.
|
|
||||||
|
|
||||||
## [Internal] Features to consider adding to this Action
|
|
||||||
|
|
||||||
These are some features which should likely be part of this implementation.
|
|
||||||
|
|
||||||
- [ ] Parsing SemVer more strictly, based on what formats we decide to support (e.g. we might not support prerelease tags)
|
|
||||||
- [ ] Checking that there is a valid `action.yml` file in the repository running the action, failing if it is not an Action repository
|
|
||||||
- Could be as simple as just checking for the file, or could be validating it is yml with expected actions components
|
|
||||||
- [ ] Sanitising and validating relative paths passed into the `path` arg
|
|
||||||
- For example, checking that they're inside the repository of the action and not other places on the filsystem
|
|
||||||
- [ ] Integration with Attestations API (speak to `phillmv` about this)
|
|
||||||
- [ ] A way to dynamically work out the Container Registry's URL rather than relying on the user to provide it
|
|
||||||
- Options seem to be an API endpoint or an environment variable in the runner, but this might be difficult for backwards compatibility
|
|
||||||
|
|||||||
+139
-46
@@ -1,10 +1,122 @@
|
|||||||
import * as fsHelper from '../src/fs-helper'
|
import * as fsHelper from '../src/fs-helper'
|
||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
|
import * as path from 'path'
|
||||||
import * as os from 'os'
|
import * as os from 'os'
|
||||||
import { execSync } from 'child_process'
|
import { execSync } from 'child_process'
|
||||||
|
|
||||||
const fileContent = 'This is the content of the file'
|
const fileContent = 'This is the content of the file'
|
||||||
|
|
||||||
|
describe('getConsolidatedDirectory', () => {
|
||||||
|
let sourceDir: string
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
sourceDir = `.` // fsHelper.createTempDir()
|
||||||
|
fs.mkdirSync(`${sourceDir}/folder1`)
|
||||||
|
fs.mkdirSync(`${sourceDir}/folder2`)
|
||||||
|
fs.mkdirSync(`${sourceDir}/folder2/folder3`)
|
||||||
|
fs.writeFileSync(`${sourceDir}/file0.txt`, fileContent)
|
||||||
|
fs.writeFileSync(`${sourceDir}/folder1/file1.txt`, fileContent)
|
||||||
|
fs.writeFileSync(`${sourceDir}/folder2/file2.txt`, fileContent)
|
||||||
|
fs.writeFileSync(`${sourceDir}/folder2/folder3/file3.txt`, fileContent)
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {})
|
||||||
|
|
||||||
|
afterEach(() => {})
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
fs.rmSync(`file0.txt`)
|
||||||
|
fs.rmSync(`folder1`, { recursive: true })
|
||||||
|
fs.rmSync(`folder2`, { recursive: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns the directory itself if it is a single directory, and instructed to not clean it up', () => {
|
||||||
|
// TODO: In these tests, we're not really distinguishing between the `publish-action-package` directory and the consumer repo directory, i.e., they share the same space.
|
||||||
|
// In real life, when the consumer workflow runs, its own javascript is in ., but
|
||||||
|
// the publish-action-package's code is in ${{github.action_path}}.
|
||||||
|
// So.... I guess to emulate this, we should create a temp directory (representing the consumer repo)
|
||||||
|
// and cd there before the test starts?
|
||||||
|
const { consolidatedPath, needToCleanUpDir } =
|
||||||
|
fsHelper.getConsolidatedDirectory('.')
|
||||||
|
|
||||||
|
expect(needToCleanUpDir).toBe(false)
|
||||||
|
expect(consolidatedPath).toBe('.')
|
||||||
|
expect(fsHelper.readFileContents(`file0.txt`).toString()).toEqual(
|
||||||
|
fileContent
|
||||||
|
)
|
||||||
|
expect(fsHelper.readFileContents(`folder1/file1.txt`).toString()).toEqual(
|
||||||
|
fileContent
|
||||||
|
)
|
||||||
|
expect(fsHelper.readFileContents(`folder2/file2.txt`).toString()).toEqual(
|
||||||
|
fileContent
|
||||||
|
)
|
||||||
|
expect(
|
||||||
|
fsHelper.readFileContents(`folder2/folder3/file3.txt`).toString()
|
||||||
|
).toEqual(fileContent)
|
||||||
|
})
|
||||||
|
it('returns a new directory containing copies of the multiple paths if they are legally specified, and instruct to clean it up', () => {
|
||||||
|
const { consolidatedPath, needToCleanUpDir } =
|
||||||
|
fsHelper.getConsolidatedDirectory('file0.txt folder1')
|
||||||
|
|
||||||
|
expect(needToCleanUpDir).toBe(true)
|
||||||
|
expect(consolidatedPath).not.toBe('.')
|
||||||
|
expect(
|
||||||
|
fsHelper
|
||||||
|
.readFileContents(path.join(consolidatedPath, `file0.txt`))
|
||||||
|
.toString()
|
||||||
|
).toEqual(fileContent)
|
||||||
|
expect(
|
||||||
|
fsHelper
|
||||||
|
.readFileContents(path.join(consolidatedPath, `folder1/file1.txt`))
|
||||||
|
.toString()
|
||||||
|
).toEqual(fileContent)
|
||||||
|
expect(
|
||||||
|
fs.existsSync(path.join(consolidatedPath, `folder2/file2.txt`))
|
||||||
|
).toEqual(false)
|
||||||
|
expect(
|
||||||
|
fs.existsSync(path.join(consolidatedPath, `folder2/folder3/file3.txt`))
|
||||||
|
).toEqual(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('what happens here?', () => {
|
||||||
|
const { consolidatedPath, needToCleanUpDir } =
|
||||||
|
fsHelper.getConsolidatedDirectory('folder1 folder2/folder3')
|
||||||
|
|
||||||
|
expect(needToCleanUpDir).toBe(true)
|
||||||
|
expect(consolidatedPath).not.toBe('.')
|
||||||
|
expect(fs.existsSync(path.join(consolidatedPath, `file0.txt`))).toEqual(
|
||||||
|
false
|
||||||
|
)
|
||||||
|
expect(
|
||||||
|
fsHelper
|
||||||
|
.readFileContents(path.join(consolidatedPath, `folder1/file1.txt`))
|
||||||
|
.toString()
|
||||||
|
).toEqual(fileContent)
|
||||||
|
expect(
|
||||||
|
fs.existsSync(path.join(consolidatedPath, `folder2/file2.txt`))
|
||||||
|
).toEqual(false)
|
||||||
|
expect(
|
||||||
|
fsHelper
|
||||||
|
.readFileContents(path.join(consolidatedPath, `folder3/file3.txt`))
|
||||||
|
.toString()
|
||||||
|
).toEqual(fileContent) // <--- TODO: This is what I'm unsure of
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws an error for illegal path spec - single', () => {
|
||||||
|
expect(() => {
|
||||||
|
fsHelper.getConsolidatedDirectory('folder4')
|
||||||
|
}).toThrow('filePath folder4 does not exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws an error for illegal path spec - multiple', () => {
|
||||||
|
expect(() => {
|
||||||
|
fsHelper.getConsolidatedDirectory('folder1 folder4')
|
||||||
|
}).toThrow('filePath folder4 does not exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: consider doing the thing Michael suggested where we exclude directories starting with .
|
||||||
|
})
|
||||||
|
|
||||||
describe('createArchives', () => {
|
describe('createArchives', () => {
|
||||||
let tmpDir: string
|
let tmpDir: string
|
||||||
let distDir: string
|
let distDir: string
|
||||||
@@ -74,8 +186,6 @@ describe('createArchives', () => {
|
|||||||
expect(zipSHA).toEqual(systemZipHash)
|
expect(zipSHA).toEqual(systemZipHash)
|
||||||
expect(tarSHA).toEqual(systemTarHash)
|
expect(tarSHA).toEqual(systemTarHash)
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: Test the failure cases
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('createTempDir', () => {
|
describe('createTempDir', () => {
|
||||||
@@ -133,6 +243,33 @@ describe('isDirectory', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('isActionRepo', () => {
|
||||||
|
let stagingDir: string
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
stagingDir = fsHelper.createTempDir()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fs.rmSync(stagingDir, { recursive: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns true if action.yml exists at the root', () => {
|
||||||
|
fs.writeFileSync(path.join(stagingDir, `action.yml`), fileContent)
|
||||||
|
expect(fsHelper.isActionRepo(stagingDir)).toEqual(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns true if action.yaml exists at the root', () => {
|
||||||
|
fs.writeFileSync(path.join(stagingDir, `action.yaml`), fileContent)
|
||||||
|
expect(fsHelper.isActionRepo(stagingDir)).toEqual(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns false if action.y(a)ml doesn't exist at the root", () => {
|
||||||
|
fs.writeFileSync(path.join(stagingDir, `action.yaaml`), fileContent)
|
||||||
|
expect(fsHelper.isActionRepo(stagingDir)).toEqual(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('readFileContents', () => {
|
describe('readFileContents', () => {
|
||||||
let dir: string
|
let dir: string
|
||||||
|
|
||||||
@@ -170,47 +307,3 @@ describe('removeDir', () => {
|
|||||||
expect(fs.existsSync(dir)).toEqual(false)
|
expect(fs.existsSync(dir)).toEqual(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('bundleFilesintoDirectory', () => {
|
|
||||||
let sourceDir: string
|
|
||||||
let targetDir: string
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
sourceDir = fsHelper.createTempDir()
|
|
||||||
targetDir = fsHelper.createTempDir()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
fs.rmSync(sourceDir, { recursive: true })
|
|
||||||
fs.rmSync(targetDir, { recursive: true })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('bundles files and folders into a directory', () => {
|
|
||||||
// Create some test files and folders in the sourceDir
|
|
||||||
const file1 = `${sourceDir}/file1.txt`
|
|
||||||
const folder1 = `${sourceDir}/folder1`
|
|
||||||
const file2 = `${folder1}/file3.txt`
|
|
||||||
|
|
||||||
fs.mkdirSync(folder1)
|
|
||||||
fs.writeFileSync(file1, fileContent)
|
|
||||||
fs.writeFileSync(file2, fileContent)
|
|
||||||
|
|
||||||
// Bundle the files and folders into the targetDir
|
|
||||||
fsHelper.bundleFilesintoDirectory([file1, folder1], targetDir)
|
|
||||||
|
|
||||||
// Check that the files and folders were copied
|
|
||||||
expect(fs.existsSync(file1)).toEqual(true)
|
|
||||||
expect(fsHelper.readFileContents(file1).toString()).toEqual(fileContent)
|
|
||||||
|
|
||||||
expect(fs.existsSync(`${targetDir}/folder1`)).toEqual(true)
|
|
||||||
|
|
||||||
expect(fs.existsSync(file2)).toEqual(true)
|
|
||||||
expect(fsHelper.readFileContents(file2).toString()).toEqual(fileContent)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('throws an error if a file or directory does not exist', () => {
|
|
||||||
expect(() => {
|
|
||||||
fsHelper.bundleFilesintoDirectory(['/does/not/exist'], targetDir)
|
|
||||||
}).toThrow('File /does/not/exist does not exist')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|||||||
+107
-32
@@ -9,20 +9,21 @@ let axiosPostMock: jest.SpyInstance
|
|||||||
let axiosPutMock: jest.SpyInstance
|
let axiosPutMock: jest.SpyInstance
|
||||||
let axiosHeadMock: jest.SpyInstance
|
let axiosHeadMock: jest.SpyInstance
|
||||||
|
|
||||||
const token = '1234567890'
|
const token = 'test-token'
|
||||||
const registry = new URL('https://ghcr.io')
|
const registry = new URL('https://ghcr.io')
|
||||||
const repository = 'test/test'
|
const repository = 'test-org/test-repo'
|
||||||
const releaseId = '1234567890'
|
const releaseId = 'test-release-id'
|
||||||
const semver = '1.0.0'
|
const semver = '1.2.3'
|
||||||
|
const genericSha = '1234567890' // We should look at using different shas here to catch bug, but that make location validation harder
|
||||||
const zipFile: fsHelper.FileMetadata = {
|
const zipFile: fsHelper.FileMetadata = {
|
||||||
path: 'test-repo-1.0.0.zip',
|
path: `test-repo-${semver}.zip`,
|
||||||
size: 100,
|
size: 123,
|
||||||
sha256: '1234567890'
|
sha256: genericSha
|
||||||
}
|
}
|
||||||
const tarFile: fsHelper.FileMetadata = {
|
const tarFile: fsHelper.FileMetadata = {
|
||||||
path: 'test-repo-1.0.0.tar.gz',
|
path: `test-repo-${semver}.tar.gz`,
|
||||||
size: 100,
|
size: 456,
|
||||||
sha256: '1234567890'
|
sha256: genericSha
|
||||||
}
|
}
|
||||||
|
|
||||||
const testManifest: ociContainer.Manifest = {
|
const testManifest: ociContainer.Manifest = {
|
||||||
@@ -50,25 +51,25 @@ const testManifest: ociContainer.Manifest = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
mediaType: 'application/vnd.github.actions.package.layer.v1.tar+gzip',
|
mediaType: 'application/vnd.github.actions.package.layer.v1.tar+gzip',
|
||||||
size: 100,
|
size: tarFile.size,
|
||||||
digest: 'sha256:1234567890',
|
digest: `sha256:${tarFile.sha256}`,
|
||||||
annotations: {
|
annotations: {
|
||||||
'org.opencontainers.image.title': 'test-repo-1.0.0.tar.gz'
|
'org.opencontainers.image.title': tarFile.path
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
mediaType: 'application/vnd.github.actions.package.layer.v1.zip',
|
mediaType: 'application/vnd.github.actions.package.layer.v1.zip',
|
||||||
size: 100,
|
size: zipFile.size,
|
||||||
digest: 'sha256:1234567890',
|
digest: `sha256:${zipFile.sha256}`,
|
||||||
annotations: {
|
annotations: {
|
||||||
'org.opencontainers.image.title': 'test-repo-1.0.0.zip'
|
'org.opencontainers.image.title': zipFile.path
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
annotations: {
|
annotations: {
|
||||||
'org.opencontainers.image.created': '2021-01-01T00:00:00.000Z',
|
'org.opencontainers.image.created': '2021-01-01T00:00:00.000Z',
|
||||||
'action.tar.gz.digest': '1234567890',
|
'action.tar.gz.digest': tarFile.sha256,
|
||||||
'action.zip.digest': '1234567890',
|
'action.zip.digest': zipFile.sha256,
|
||||||
'com.github.package.type': 'actions_oci_pkg'
|
'com.github.package.type': 'actions_oci_pkg'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,7 +102,7 @@ describe('publishOCIArtifact', () => {
|
|||||||
return {
|
return {
|
||||||
status: 202,
|
status: 202,
|
||||||
headers: {
|
headers: {
|
||||||
location: 'https://ghcr.io/v2/test/test/blobs/uploads/1234567890'
|
location: `https://ghcr.io/v2/${repository}/blobs/uploads/${genericSha}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -133,11 +134,9 @@ describe('publishOCIArtifact', () => {
|
|||||||
expect(axiosHeadMock).toHaveBeenCalledTimes(3)
|
expect(axiosHeadMock).toHaveBeenCalledTimes(3)
|
||||||
expect(axiosPostMock).toHaveBeenCalledTimes(3)
|
expect(axiosPostMock).toHaveBeenCalledTimes(3)
|
||||||
expect(axiosPutMock).toHaveBeenCalledTimes(4)
|
expect(axiosPutMock).toHaveBeenCalledTimes(4)
|
||||||
|
|
||||||
// TODO: Check that the base64 encoded token is sent in the Authorization header
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('skips uploading layer blobs that already exist', async () => {
|
it('skips uploading all layer blobs when they all already exist', async () => {
|
||||||
// Simulate all blobs already existing
|
// Simulate all blobs already existing
|
||||||
axiosHeadMock.mockImplementation(async (url, config) => {
|
axiosHeadMock.mockImplementation(async (url, config) => {
|
||||||
validateRequestConfig(200, url, config)
|
validateRequestConfig(200, url, config)
|
||||||
@@ -152,7 +151,7 @@ describe('publishOCIArtifact', () => {
|
|||||||
return {
|
return {
|
||||||
status: 202,
|
status: 202,
|
||||||
headers: {
|
headers: {
|
||||||
location: 'https://ghcr.io/v2/test/test/blobs/uploads/1234567890'
|
location: `https://ghcr.io/v2/${repository}/blobs/uploads/${genericSha}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -187,6 +186,68 @@ describe('publishOCIArtifact', () => {
|
|||||||
expect(axiosPutMock).toHaveBeenCalledTimes(1)
|
expect(axiosPutMock).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('skips uploading layer blobs that already exist', async () => {
|
||||||
|
// Simulate some blobs already existing
|
||||||
|
|
||||||
|
let count = 0
|
||||||
|
axiosHeadMock.mockImplementation(async (url, config) => {
|
||||||
|
count++
|
||||||
|
if (count === 1) {
|
||||||
|
// report the first blob as being there
|
||||||
|
validateRequestConfig(200, url, config)
|
||||||
|
return {
|
||||||
|
status: 200
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// report all others are missing
|
||||||
|
validateRequestConfig(404, url, config)
|
||||||
|
return {
|
||||||
|
status: 404
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Simulate successful initiation of uploads for all blobs & return location
|
||||||
|
axiosPostMock.mockImplementation(async (url, data, config) => {
|
||||||
|
validateRequestConfig(202, url, config)
|
||||||
|
return {
|
||||||
|
status: 202,
|
||||||
|
headers: {
|
||||||
|
location: `https://ghcr.io/v2/${repository}/blobs/uploads/${genericSha}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Simulate successful reading of all the files
|
||||||
|
fsReadFileSyncMock.mockImplementation(() => {
|
||||||
|
return Buffer.from('test')
|
||||||
|
})
|
||||||
|
|
||||||
|
// Simulate successful upload of all blobs & then the manifest
|
||||||
|
axiosPutMock.mockImplementation(async (url, data, config) => {
|
||||||
|
validateRequestConfig(201, url, config)
|
||||||
|
return {
|
||||||
|
status: 201
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await publishOCIArtifact(
|
||||||
|
token,
|
||||||
|
registry,
|
||||||
|
repository,
|
||||||
|
releaseId,
|
||||||
|
semver,
|
||||||
|
zipFile,
|
||||||
|
tarFile,
|
||||||
|
testManifest
|
||||||
|
)
|
||||||
|
|
||||||
|
// We should only head all the blobs and then upload the missing blobs and manifest
|
||||||
|
expect(axiosHeadMock).toHaveBeenCalledTimes(3)
|
||||||
|
expect(axiosPostMock).toHaveBeenCalledTimes(2)
|
||||||
|
expect(axiosPutMock).toHaveBeenCalledTimes(3)
|
||||||
|
})
|
||||||
|
|
||||||
it('throws an error if checking for existing blobs fails', async () => {
|
it('throws an error if checking for existing blobs fails', async () => {
|
||||||
// Simulate failed response code
|
// Simulate failed response code
|
||||||
axiosHeadMock.mockImplementation(async (url, config) => {
|
axiosHeadMock.mockImplementation(async (url, config) => {
|
||||||
@@ -288,7 +349,7 @@ describe('publishOCIArtifact', () => {
|
|||||||
return {
|
return {
|
||||||
status: 202,
|
status: 202,
|
||||||
headers: {
|
headers: {
|
||||||
location: 'https://ghcr.io/v2/test/test/blobs/uploads/1234567890'
|
location: `https://ghcr.io/v2/${repository}/blobs/uploads/${genericSha}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -335,7 +396,7 @@ describe('publishOCIArtifact', () => {
|
|||||||
return {
|
return {
|
||||||
status: 202,
|
status: 202,
|
||||||
headers: {
|
headers: {
|
||||||
location: 'https://ghcr.io/v2/test/test/blobs/uploads/1234567890'
|
location: `https://ghcr.io/v2/${repository}/blobs/uploads/${genericSha}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -389,7 +450,7 @@ describe('publishOCIArtifact', () => {
|
|||||||
return {
|
return {
|
||||||
status: 202,
|
status: 202,
|
||||||
headers: {
|
headers: {
|
||||||
location: 'https://ghcr.io/v2/test/test/blobs/uploads/1234567890'
|
location: `https://ghcr.io/v2/${repository}/blobs/uploads/${genericSha}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -422,9 +483,15 @@ describe('publishOCIArtifact', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('throws an error if one of the layers has the wrong media type', async () => {
|
it('throws an error if one of the layers has the wrong media type', async () => {
|
||||||
const modifiedTestManifest = testManifest
|
const modifiedTestManifest = { ...testManifest } // This is _NOT_ a deep clone
|
||||||
|
modifiedTestManifest.layers = cloneLayers(modifiedTestManifest.layers)
|
||||||
modifiedTestManifest.layers[0].mediaType = 'application/json'
|
modifiedTestManifest.layers[0].mediaType = 'application/json'
|
||||||
|
|
||||||
|
// just checking to make sure we are not changing the shared object
|
||||||
|
expect(modifiedTestManifest.layers[0].mediaType).not.toEqual(
|
||||||
|
testManifest.layers[0].mediaType
|
||||||
|
)
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
publishOCIArtifact(
|
publishOCIArtifact(
|
||||||
token,
|
token,
|
||||||
@@ -434,7 +501,7 @@ describe('publishOCIArtifact', () => {
|
|||||||
semver,
|
semver,
|
||||||
zipFile,
|
zipFile,
|
||||||
tarFile,
|
tarFile,
|
||||||
testManifest
|
modifiedTestManifest
|
||||||
)
|
)
|
||||||
).rejects.toThrow('Unknown media type application/json')
|
).rejects.toThrow('Unknown media type application/json')
|
||||||
})
|
})
|
||||||
@@ -446,12 +513,12 @@ describe('publishOCIArtifact', () => {
|
|||||||
function validateRequestConfig(status: number, url: string, config: any): void {
|
function validateRequestConfig(status: number, url: string, config: any): void {
|
||||||
// Basic URL checks
|
// Basic URL checks
|
||||||
expect(url).toBeDefined()
|
expect(url).toBeDefined()
|
||||||
|
|
||||||
if (!url.startsWith(registry.toString())) {
|
if (!url.startsWith(registry.toString())) {
|
||||||
console.log(url)
|
console.log(`${url} does not start with ${registry}`)
|
||||||
}
|
}
|
||||||
|
// if these expect fails, run the test again with `-- --silent=false`
|
||||||
expect(url.startsWith(registry.toString())).toBe(true)
|
// the console.log above should give a clue about which URL is failing
|
||||||
|
expect(url.startsWith(registry.toString())).toBeTruthy()
|
||||||
|
|
||||||
// Config checks
|
// Config checks
|
||||||
expect(config).toBeDefined()
|
expect(config).toBeDefined()
|
||||||
@@ -472,3 +539,11 @@ function validateRequestConfig(status: number, url: string, config: any): void {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cloneLayers(layers: ociContainer.Layer[]): ociContainer.Layer[] {
|
||||||
|
const result: ociContainer.Layer[] = []
|
||||||
|
for (const layer of layers) {
|
||||||
|
result.push({ ...layer }) // this is _NOT_ a deep clone
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|||||||
+32
-31
@@ -20,10 +20,10 @@ let setOutputMock: jest.SpyInstance
|
|||||||
|
|
||||||
// Mock the filesystem helper
|
// Mock the filesystem helper
|
||||||
let createTempDirMock: jest.SpyInstance
|
let createTempDirMock: jest.SpyInstance
|
||||||
let isDirectoryMock: jest.SpyInstance
|
|
||||||
let createArchivesMock: jest.SpyInstance
|
let createArchivesMock: jest.SpyInstance
|
||||||
let removeDirMock: jest.SpyInstance
|
let removeDirMock: jest.SpyInstance
|
||||||
let bundleFilesintoDirectoryMock: jest.SpyInstance
|
let getConsolidatedDirectoryMock: jest.SpyInstance
|
||||||
|
let isActionRepoMock: jest.SpyInstance
|
||||||
|
|
||||||
// Mock the GHCR Client
|
// Mock the GHCR Client
|
||||||
let publishOCIArtifactMock: jest.SpyInstance
|
let publishOCIArtifactMock: jest.SpyInstance
|
||||||
@@ -41,14 +41,14 @@ describe('action', () => {
|
|||||||
createTempDirMock = jest
|
createTempDirMock = jest
|
||||||
.spyOn(fsHelper, 'createTempDir')
|
.spyOn(fsHelper, 'createTempDir')
|
||||||
.mockImplementation()
|
.mockImplementation()
|
||||||
isDirectoryMock = jest.spyOn(fsHelper, 'isDirectory').mockImplementation()
|
|
||||||
createArchivesMock = jest
|
createArchivesMock = jest
|
||||||
.spyOn(fsHelper, 'createArchives')
|
.spyOn(fsHelper, 'createArchives')
|
||||||
.mockImplementation()
|
.mockImplementation()
|
||||||
removeDirMock = jest.spyOn(fsHelper, 'removeDir').mockImplementation()
|
removeDirMock = jest.spyOn(fsHelper, 'removeDir').mockImplementation()
|
||||||
bundleFilesintoDirectoryMock = jest
|
getConsolidatedDirectoryMock = jest
|
||||||
.spyOn(fsHelper, 'bundleFilesintoDirectory')
|
.spyOn(fsHelper, 'getConsolidatedDirectory')
|
||||||
.mockImplementation()
|
.mockImplementation()
|
||||||
|
isActionRepoMock = jest.spyOn(fsHelper, 'isActionRepo').mockImplementation()
|
||||||
|
|
||||||
// GHCR Client mocks
|
// GHCR Client mocks
|
||||||
publishOCIArtifactMock = jest
|
publishOCIArtifactMock = jest
|
||||||
@@ -61,7 +61,7 @@ describe('action', () => {
|
|||||||
process.env.GITHUB_REPOSITORY = ''
|
process.env.GITHUB_REPOSITORY = ''
|
||||||
|
|
||||||
// Run the action
|
// Run the action
|
||||||
await main.run()
|
await main.run('directory1 directory2')
|
||||||
|
|
||||||
// Check the results
|
// Check the results
|
||||||
expect(setFailedMock).toHaveBeenCalledWith('Could not find Repository.')
|
expect(setFailedMock).toHaveBeenCalledWith('Could not find Repository.')
|
||||||
@@ -69,11 +69,11 @@ describe('action', () => {
|
|||||||
|
|
||||||
it('fails if event is not a release', async () => {
|
it('fails if event is not a release', async () => {
|
||||||
// Mock the environment
|
// Mock the environment
|
||||||
process.env.GITHUB_REPOSITORY = 'test/test'
|
process.env.GITHUB_REPOSITORY = 'test-org/test-repo'
|
||||||
github.context.eventName = 'push'
|
github.context.eventName = 'push'
|
||||||
|
|
||||||
// Run the action
|
// Run the action
|
||||||
await main.run()
|
await main.run('directory1 directory2')
|
||||||
|
|
||||||
// Check the results
|
// Check the results
|
||||||
expect(setFailedMock).toHaveBeenCalledWith(
|
expect(setFailedMock).toHaveBeenCalledWith(
|
||||||
@@ -83,7 +83,7 @@ describe('action', () => {
|
|||||||
|
|
||||||
it('fails if release tag is not a valid semantic version', async () => {
|
it('fails if release tag is not a valid semantic version', async () => {
|
||||||
// Mock the environment
|
// Mock the environment
|
||||||
process.env.GITHUB_REPOSITORY = 'test/test'
|
process.env.GITHUB_REPOSITORY = 'test-org/test-repo'
|
||||||
github.context.eventName = 'release'
|
github.context.eventName = 'release'
|
||||||
github.context.payload = {
|
github.context.payload = {
|
||||||
release: {
|
release: {
|
||||||
@@ -93,7 +93,7 @@ describe('action', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Run the action
|
// Run the action
|
||||||
await main.run()
|
await main.run('directory1 directory2')
|
||||||
|
|
||||||
// Check the results
|
// Check the results
|
||||||
expect(setFailedMock).toHaveBeenCalledWith(
|
expect(setFailedMock).toHaveBeenCalledWith(
|
||||||
@@ -103,12 +103,12 @@ describe('action', () => {
|
|||||||
|
|
||||||
it('fails if multiple paths are provided and staging files fails', async () => {
|
it('fails if multiple paths are provided and staging files fails', async () => {
|
||||||
// Mock the environment
|
// Mock the environment
|
||||||
process.env.GITHUB_REPOSITORY = 'test/test'
|
process.env.GITHUB_REPOSITORY = 'test-org/test-repo'
|
||||||
github.context.eventName = 'release'
|
github.context.eventName = 'release'
|
||||||
github.context.payload = {
|
github.context.payload = {
|
||||||
release: {
|
release: {
|
||||||
id: '123',
|
id: '123',
|
||||||
tag_name: 'v1.0.0'
|
tag_name: 'v1.2.3'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
getInputMock.mockImplementation((name: string) => {
|
getInputMock.mockImplementation((name: string) => {
|
||||||
@@ -120,14 +120,12 @@ describe('action', () => {
|
|||||||
return ''
|
return ''
|
||||||
})
|
})
|
||||||
|
|
||||||
isDirectoryMock.mockImplementation(() => true)
|
getConsolidatedDirectoryMock.mockImplementation(() => {
|
||||||
|
|
||||||
bundleFilesintoDirectoryMock.mockImplementation(() => {
|
|
||||||
throw new Error('Something went wrong')
|
throw new Error('Something went wrong')
|
||||||
})
|
})
|
||||||
|
|
||||||
// Run the action
|
// Run the action
|
||||||
await main.run()
|
await main.run('directory1 directory2')
|
||||||
|
|
||||||
// Check the results
|
// Check the results
|
||||||
expect(setFailedMock).toHaveBeenCalledWith('Something went wrong')
|
expect(setFailedMock).toHaveBeenCalledWith('Something went wrong')
|
||||||
@@ -135,12 +133,12 @@ describe('action', () => {
|
|||||||
|
|
||||||
it('fails if an error is thrown from dependent code', async () => {
|
it('fails if an error is thrown from dependent code', async () => {
|
||||||
// Mock the environment
|
// Mock the environment
|
||||||
process.env.GITHUB_REPOSITORY = 'test/test'
|
process.env.GITHUB_REPOSITORY = 'test-org/test-repo'
|
||||||
github.context.eventName = 'release'
|
github.context.eventName = 'release'
|
||||||
github.context.payload = {
|
github.context.payload = {
|
||||||
release: {
|
release: {
|
||||||
id: '123',
|
id: '123',
|
||||||
tag_name: 'v1.0.0'
|
tag_name: 'v1.2.3'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
getInputMock.mockImplementation((name: string) => {
|
getInputMock.mockImplementation((name: string) => {
|
||||||
@@ -152,7 +150,10 @@ describe('action', () => {
|
|||||||
return ''
|
return ''
|
||||||
})
|
})
|
||||||
|
|
||||||
isDirectoryMock.mockImplementation(() => true)
|
getConsolidatedDirectoryMock.mockImplementation(() => {
|
||||||
|
return { consolidatedDirectory: '/tmp/test', needToCleanUpDir: false }
|
||||||
|
})
|
||||||
|
isActionRepoMock.mockImplementation(() => true)
|
||||||
|
|
||||||
createTempDirMock.mockImplementation(() => '/tmp/test')
|
createTempDirMock.mockImplementation(() => '/tmp/test')
|
||||||
|
|
||||||
@@ -161,10 +162,10 @@ describe('action', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Run the action
|
// Run the action
|
||||||
await main.run()
|
await main.run('directory')
|
||||||
|
|
||||||
// Check the results
|
// Check the results
|
||||||
expect(isDirectoryMock).toHaveBeenCalledWith('directory')
|
expect(getConsolidatedDirectoryMock).toHaveBeenCalledTimes(1)
|
||||||
expect(setFailedMock).toHaveBeenCalledWith('Something went wrong')
|
expect(setFailedMock).toHaveBeenCalledWith('Something went wrong')
|
||||||
|
|
||||||
// Expect the files to be cleaned up
|
// Expect the files to be cleaned up
|
||||||
@@ -172,22 +173,22 @@ describe('action', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('successfully uploads if the release tag is a semver without v prefix', async () => {
|
it('successfully uploads if the release tag is a semver without v prefix', async () => {
|
||||||
await testHappyPath('1.0.0', 'test')
|
await testHappyPath('1.2.3', 'test')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('successfully uploads if the release tag is a semver with v prefix', async () => {
|
it('successfully uploads if the release tag is a semver with v prefix', async () => {
|
||||||
await testHappyPath('v1.0.0', 'test')
|
await testHappyPath('v1.2.3', 'test')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('successfully uploads if multiple paths are provided', async () => {
|
it('successfully uploads if multiple paths are provided', async () => {
|
||||||
await testHappyPath('v1.0.0', 'test test2')
|
await testHappyPath('v1.2.3', 'test test2')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Test that main successfully uploads and returns the manifest & package URL
|
// Test that main successfully uploads and returns the manifest & package URL
|
||||||
async function testHappyPath(version: string, path: string): Promise<void> {
|
async function testHappyPath(version: string, path: string): Promise<void> {
|
||||||
// Mock the environment
|
// Mock the environment
|
||||||
process.env.GITHUB_REPOSITORY = 'test/test'
|
process.env.GITHUB_REPOSITORY = 'test-org/test-repo'
|
||||||
github.context.eventName = 'release'
|
github.context.eventName = 'release'
|
||||||
github.context.payload = {
|
github.context.payload = {
|
||||||
release: {
|
release: {
|
||||||
@@ -204,10 +205,10 @@ async function testHappyPath(version: string, path: string): Promise<void> {
|
|||||||
return ''
|
return ''
|
||||||
})
|
})
|
||||||
|
|
||||||
isDirectoryMock.mockImplementation(() => true)
|
isActionRepoMock.mockImplementation(() => true)
|
||||||
|
|
||||||
bundleFilesintoDirectoryMock.mockImplementation(() => {
|
getConsolidatedDirectoryMock.mockImplementation(() => {
|
||||||
return '/tmp/test'
|
return { consolidatedDirectory: '/tmp/test', needToCleanUpDir: false } // TODO: I don't understand why I have to name the variables here but not in the implementation code
|
||||||
})
|
})
|
||||||
|
|
||||||
createTempDirMock.mockImplementation(() => '/tmp/test')
|
createTempDirMock.mockImplementation(() => '/tmp/test')
|
||||||
@@ -228,18 +229,18 @@ async function testHappyPath(version: string, path: string): Promise<void> {
|
|||||||
})
|
})
|
||||||
|
|
||||||
publishOCIArtifactMock.mockImplementation(() => {
|
publishOCIArtifactMock.mockImplementation(() => {
|
||||||
return new URL('https://ghcr.io/v2/test/test:1.0.0')
|
return new URL('https://ghcr.io/v2/test-org/test-repo:1.2.3')
|
||||||
})
|
})
|
||||||
|
|
||||||
// Run the action
|
// Run the action
|
||||||
await main.run()
|
await main.run(path)
|
||||||
|
|
||||||
expect(publishOCIArtifactMock).toHaveBeenCalledTimes(1)
|
expect(publishOCIArtifactMock).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
// Check manifest is in output
|
// Check manifest is in output
|
||||||
expect(setOutputMock).toHaveBeenCalledWith(
|
expect(setOutputMock).toHaveBeenCalledWith(
|
||||||
'package-url',
|
'package-url',
|
||||||
'https://ghcr.io/v2/test/test:1.0.0'
|
'https://ghcr.io/v2/test-org/test-repo:1.2.3'
|
||||||
)
|
)
|
||||||
expect(setOutputMock).toHaveBeenCalledWith(
|
expect(setOutputMock).toHaveBeenCalledWith(
|
||||||
'package-manifest',
|
'package-manifest',
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
import { createActionPackageManifest } from '../src/oci-container'
|
import { createActionPackageManifest } from '../src/oci-container'
|
||||||
import { FileMetadata } from '../src/fs-helper'
|
import { FileMetadata } from '../src/fs-helper'
|
||||||
|
|
||||||
describe('createActionPackageManigest', () => {
|
describe('createActionPackageManifest', () => {
|
||||||
it('creates a manifest containing the provided information', () => {
|
it('creates a manifest containing the provided information', () => {
|
||||||
const date = new Date()
|
const date = new Date()
|
||||||
const repo = 'test-org/test-repo'
|
const repo = 'test-org/test-repo'
|
||||||
const sanitizedRepo = 'test-org-test-repo'
|
const sanitizedRepo = 'test-org-test-repo'
|
||||||
const version = '1.0.0'
|
const version = '1.2.3'
|
||||||
const tarFile: FileMetadata = {
|
const tarFile: FileMetadata = {
|
||||||
path: '/test/test/test',
|
path: '/test/test/test.tar.gz',
|
||||||
sha256: '1234567890',
|
sha256: 'tarSha',
|
||||||
size: 100
|
size: 123
|
||||||
}
|
}
|
||||||
const zipFile: FileMetadata = {
|
const zipFile: FileMetadata = {
|
||||||
path: '/test/test/test',
|
path: '/test/test/test.zip',
|
||||||
sha256: '1234567890',
|
sha256: 'zipSha',
|
||||||
size: 100
|
size: 456
|
||||||
}
|
}
|
||||||
|
|
||||||
const expectedJSON = `{
|
const expectedJSON = `{
|
||||||
@@ -49,8 +49,8 @@ describe('createActionPackageManigest', () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"mediaType":"application/vnd.github.actions.package.layer.v1.zip",
|
"mediaType":"application/vnd.github.actions.package.layer.v1.zip",
|
||||||
"size":${tarFile.size},
|
"size":${zipFile.size},
|
||||||
"digest":"${tarFile.sha256}",
|
"digest":"${zipFile.sha256}",
|
||||||
"annotations":{
|
"annotations":{
|
||||||
"org.opencontainers.image.title":"${sanitizedRepo}_${version}.zip"
|
"org.opencontainers.image.title":"${sanitizedRepo}_${version}.zip"
|
||||||
}
|
}
|
||||||
@@ -67,13 +67,13 @@ describe('createActionPackageManigest', () => {
|
|||||||
const manifest = createActionPackageManifest(
|
const manifest = createActionPackageManifest(
|
||||||
{
|
{
|
||||||
path: 'test.tar.gz',
|
path: 'test.tar.gz',
|
||||||
size: 100,
|
size: tarFile.size,
|
||||||
sha256: '1234567890'
|
sha256: tarFile.sha256
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'test.zip',
|
path: 'test.zip',
|
||||||
size: 100,
|
size: zipFile.size,
|
||||||
sha256: '1234567890'
|
sha256: zipFile.sha256
|
||||||
},
|
},
|
||||||
repo,
|
repo,
|
||||||
version,
|
version,
|
||||||
|
|||||||
+28
-11
@@ -7,25 +7,42 @@ branding:
|
|||||||
color: 'red'
|
color: 'red'
|
||||||
|
|
||||||
inputs:
|
inputs:
|
||||||
token:
|
|
||||||
required: true
|
|
||||||
description: >
|
|
||||||
GITHUB_TOKEN having the write:package scope to publish an action package to GHCR.
|
|
||||||
default: ${{ github.token }}
|
|
||||||
path:
|
path:
|
||||||
required: false
|
required: false
|
||||||
description: The work directory or path to be tar archived and uploaded as OCI Artifact layer.
|
description: The work directory or path to be tar archived and uploaded as OCI Artifact layer.
|
||||||
default: '.'
|
default: '.'
|
||||||
registry:
|
|
||||||
required: false
|
|
||||||
description: The registry to publish the action package to.
|
|
||||||
default: https://ghcr.io/ # TODO: this should perhaps be fetched from GitHub API.
|
|
||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
package-url:
|
package-url:
|
||||||
description: 'The name of package published to GHCR along with semver. For example, https://ghcr.io/actions/package-action:1.0.1'
|
description: 'The name of package published to GHCR along with semver. For example, https://ghcr.io/actions/package-action:1.0.1'
|
||||||
|
value: ${{steps.publish.outputs.package-url}}
|
||||||
package-manifest:
|
package-manifest:
|
||||||
description: 'The package manifest of the published package in JSON format'
|
description: 'The package manifest of the published package in JSON format'
|
||||||
|
value: ${{steps.publish.outputs.package-manifest}}
|
||||||
|
package-manifest-sha:
|
||||||
|
description: 'A sha256 hash of the package manifest'
|
||||||
|
value: ${{steps.publish.outputs.package-manifest-sha}}
|
||||||
runs:
|
runs:
|
||||||
using: 'node16'
|
using: 'composite'
|
||||||
main: 'dist/index.js'
|
steps:
|
||||||
|
- name: Publish Action Package
|
||||||
|
run: 'node ${{github.action_path}}/dist/index.js --path ${{ inputs.path }}'
|
||||||
|
shell: bash
|
||||||
|
id: publish
|
||||||
|
env:
|
||||||
|
TOKEN: ${{ github.token }}
|
||||||
|
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||||
|
- name: Output variables
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "package manifest": ${{steps.publish.outputs.package-manifest}}
|
||||||
|
echo "package manifest sha": ${{steps.publish.outputs.package-manifest-sha}}
|
||||||
|
echo "package url": ${{steps.publish.outputs.package-url}}
|
||||||
|
echo "subject name": ${{github.repository}}_${{github.ref}}
|
||||||
|
# - name: Generate Provenance Attestation
|
||||||
|
# uses: github-early-access/generate-build-provenance@main
|
||||||
|
# id: build-provenance
|
||||||
|
# with:
|
||||||
|
# subject-name: ${{github.repository}}_${{github.ref}}
|
||||||
|
# subject-digest: ${{steps.publish.outputs.package-manifest-sha}}
|
||||||
|
# push-to-registry: false
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="106" height="20" role="img" aria-label="Coverage: 100%"><title>Coverage: 100%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="106" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="63" height="20" fill="#555"/><rect x="63" width="43" height="20" fill="#4c1"/><rect width="106" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="325" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="530">Coverage</text><text x="325" y="140" transform="scale(.1)" fill="#fff" textLength="530">Coverage</text><text aria-hidden="true" x="835" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="330">100%</text><text x="835" y="140" transform="scale(.1)" fill="#fff" textLength="330">100%</text></g></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="116" height="20" role="img" aria-label="Coverage: 93.81%"><title>Coverage: 93.81%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="116" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="63" height="20" fill="#555"/><rect x="63" width="53" height="20" fill="#4c1"/><rect width="116" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="325" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="530">Coverage</text><text x="325" y="140" transform="scale(.1)" fill="#fff" textLength="530">Coverage</text><text aria-hidden="true" x="885" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">93.81%</text><text x="885" y="140" transform="scale(.1)" fill="#fff" textLength="430">93.81%</text></g></svg>
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
+144
@@ -0,0 +1,144 @@
|
|||||||
|
"use strict";
|
||||||
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||||
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||||
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||||
|
}
|
||||||
|
Object.defineProperty(o, k2, desc);
|
||||||
|
}) : (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
o[k2] = m[k];
|
||||||
|
}));
|
||||||
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||||
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||||
|
}) : function(o, v) {
|
||||||
|
o["default"] = v;
|
||||||
|
});
|
||||||
|
var __importStar = (this && this.__importStar) || function (mod) {
|
||||||
|
if (mod && mod.__esModule) return mod;
|
||||||
|
var result = {};
|
||||||
|
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||||
|
__setModuleDefault(result, mod);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||||
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.bundleFilesintoDirectory = exports.readFileContents = exports.isDirectory = exports.createArchives = exports.removeDir = exports.createTempDir = void 0;
|
||||||
|
const fs = __importStar(require("fs"));
|
||||||
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
||||||
|
const path = __importStar(require("path"));
|
||||||
|
const tar = __importStar(require("tar"));
|
||||||
|
const archiver = __importStar(require("archiver"));
|
||||||
|
const crypto = __importStar(require("crypto"));
|
||||||
|
const os = __importStar(require("os"));
|
||||||
|
function createTempDir() {
|
||||||
|
const randomDirName = crypto.randomBytes(4).toString('hex');
|
||||||
|
const tempDir = path.join(os.tmpdir(), randomDirName);
|
||||||
|
if (!fs.existsSync(tempDir)) {
|
||||||
|
fs.mkdirSync(tempDir);
|
||||||
|
}
|
||||||
|
return tempDir;
|
||||||
|
}
|
||||||
|
exports.createTempDir = createTempDir;
|
||||||
|
function removeDir(dir) {
|
||||||
|
if (fs.existsSync(dir)) {
|
||||||
|
fs.rmSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.removeDir = removeDir;
|
||||||
|
// Creates both a tar.gz and zip archive of the given directory and returns the paths to both archives (stored in the provided target directory)
|
||||||
|
// as well as the size/sha256 hash of each file.
|
||||||
|
async function createArchives(distPath, archiveTargetPath = createTempDir()) {
|
||||||
|
const zipPath = path.join(archiveTargetPath, `archive.zip`);
|
||||||
|
const tarPath = path.join(archiveTargetPath, `archive.tar.gz`);
|
||||||
|
const createZipPromise = new Promise((resolve, reject) => {
|
||||||
|
const output = fs.createWriteStream(zipPath);
|
||||||
|
const archive = archiver.create('zip');
|
||||||
|
output.on('error', (err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
archive.on('error', (err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
output.on('close', () => {
|
||||||
|
resolve(fileMetadata(zipPath));
|
||||||
|
});
|
||||||
|
archive.pipe(output);
|
||||||
|
archive.directory(distPath, false);
|
||||||
|
archive.finalize();
|
||||||
|
});
|
||||||
|
const createTarPromise = new Promise((resolve, reject) => {
|
||||||
|
tar
|
||||||
|
.c({
|
||||||
|
file: tarPath,
|
||||||
|
C: distPath, // Change to the source directory for relative paths (TODO)
|
||||||
|
gzip: true
|
||||||
|
}, ['.'])
|
||||||
|
// eslint-disable-next-line github/no-then
|
||||||
|
.catch(err => {
|
||||||
|
reject(err);
|
||||||
|
})
|
||||||
|
// eslint-disable-next-line github/no-then
|
||||||
|
.then(() => {
|
||||||
|
resolve(fileMetadata(tarPath));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const [zipFile, tarFile] = await Promise.all([
|
||||||
|
createZipPromise,
|
||||||
|
createTarPromise
|
||||||
|
]);
|
||||||
|
return { zipFile, tarFile };
|
||||||
|
}
|
||||||
|
exports.createArchives = createArchives;
|
||||||
|
function isDirectory(dirPath) {
|
||||||
|
return fs.existsSync(dirPath) && fs.lstatSync(dirPath).isDirectory();
|
||||||
|
}
|
||||||
|
exports.isDirectory = isDirectory;
|
||||||
|
function readFileContents(filePath) {
|
||||||
|
return fs.readFileSync(filePath);
|
||||||
|
}
|
||||||
|
exports.readFileContents = readFileContents;
|
||||||
|
function bundleFilesintoDirectory(files, targetDir = createTempDir()) {
|
||||||
|
for (const file of files) {
|
||||||
|
if (!fs.existsSync(file)) {
|
||||||
|
throw new Error(`File ${file} does not exist`);
|
||||||
|
}
|
||||||
|
if (isDirectory(file)) {
|
||||||
|
const targetFolder = path.join(targetDir, path.basename(file));
|
||||||
|
fs_extra_1.default.copySync(file, targetFolder);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const targetFile = path.join(targetDir, path.basename(file));
|
||||||
|
fs.copyFileSync(file, targetFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return targetDir;
|
||||||
|
}
|
||||||
|
exports.bundleFilesintoDirectory = bundleFilesintoDirectory;
|
||||||
|
// Converts a file path to a filemetadata object by querying the fs for relevant metadata.
|
||||||
|
async function fileMetadata(filePath) {
|
||||||
|
const stats = fs.statSync(filePath);
|
||||||
|
const size = stats.size;
|
||||||
|
const hash = crypto.createHash('sha256');
|
||||||
|
const fileStream = fs.createReadStream(filePath);
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
fileStream.on('data', data => {
|
||||||
|
hash.update(data);
|
||||||
|
});
|
||||||
|
fileStream.on('end', () => {
|
||||||
|
const sha256 = hash.digest('hex');
|
||||||
|
resolve({
|
||||||
|
path: filePath,
|
||||||
|
size,
|
||||||
|
sha256: `sha256:${sha256}`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
fileStream.on('error', err => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=fs-helper.js.map
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"fs-helper.js","sourceRoot":"","sources":["../src/fs-helper.ts"],"names":[],"mappings":"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"}
|
||||||
+148
@@ -0,0 +1,148 @@
|
|||||||
|
"use strict";
|
||||||
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||||
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||||
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||||
|
}
|
||||||
|
Object.defineProperty(o, k2, desc);
|
||||||
|
}) : (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
o[k2] = m[k];
|
||||||
|
}));
|
||||||
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||||
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||||
|
}) : function(o, v) {
|
||||||
|
o["default"] = v;
|
||||||
|
});
|
||||||
|
var __importStar = (this && this.__importStar) || function (mod) {
|
||||||
|
if (mod && mod.__esModule) return mod;
|
||||||
|
var result = {};
|
||||||
|
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||||
|
__setModuleDefault(result, mod);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||||
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.publishOCIArtifact = void 0;
|
||||||
|
const core = __importStar(require("@actions/core"));
|
||||||
|
const axios_1 = __importDefault(require("axios"));
|
||||||
|
const fsHelper = __importStar(require("./fs-helper"));
|
||||||
|
const axios_debug_log_1 = __importDefault(require("axios-debug-log"));
|
||||||
|
// Publish the OCI artifact and return the URL where it can be downloaded
|
||||||
|
async function publishOCIArtifact(token, registry, repository, releaseId, semver, zipFile, tarFile, manifest, debugRequests = false) {
|
||||||
|
if (debugRequests) {
|
||||||
|
configureRequestDebugLogging();
|
||||||
|
}
|
||||||
|
const b64Token = Buffer.from(token).toString('base64');
|
||||||
|
const checkBlobEndpoint = new URL(`v2/${repository}/blobs/`, registry).toString();
|
||||||
|
const uploadBlobEndpoint = new URL(`v2/${repository}/blobs/uploads/`, registry).toString();
|
||||||
|
const manifestEndpoint = new URL(`v2/${repository}/manifests/${semver}`, registry).toString();
|
||||||
|
core.info(`Creating GHCR package for release with semver:${semver} with path:"${zipFile.path}" and "${tarFile.path}".`);
|
||||||
|
const layerUploads = manifest.layers.map(async (layer) => {
|
||||||
|
switch (layer.mediaType) {
|
||||||
|
case 'application/vnd.github.actions.package.layer.v1.tar+gzip':
|
||||||
|
return uploadLayer(layer, tarFile, registry, checkBlobEndpoint, uploadBlobEndpoint, b64Token);
|
||||||
|
case 'application/vnd.github.actions.package.layer.v1.zip':
|
||||||
|
return uploadLayer(layer, zipFile, registry, checkBlobEndpoint, uploadBlobEndpoint, b64Token);
|
||||||
|
case 'application/vnd.github.actions.package.config.v1+json':
|
||||||
|
return uploadLayer(layer, { path: '', size: 0, sha256: layer.digest }, registry, checkBlobEndpoint, uploadBlobEndpoint, b64Token);
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown media type ${layer.mediaType}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await Promise.all(layerUploads);
|
||||||
|
await uploadManifest(JSON.stringify(manifest), manifestEndpoint, b64Token);
|
||||||
|
return new URL(`${repository}:${semver}`, registry);
|
||||||
|
}
|
||||||
|
exports.publishOCIArtifact = publishOCIArtifact;
|
||||||
|
async function uploadLayer(layer, file, registryURL, checkBlobEndpoint, uploadBlobEndpoint, b64Token) {
|
||||||
|
const checkExistsResponse = await axios_1.default.head(checkBlobEndpoint + layer.digest, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${b64Token}`
|
||||||
|
},
|
||||||
|
validateStatus: () => {
|
||||||
|
return true; // Allow non 2xx responses
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (checkExistsResponse.status === 200 ||
|
||||||
|
checkExistsResponse.status === 202) {
|
||||||
|
core.info(`Layer ${layer.digest} already exists. Skipping upload.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (checkExistsResponse.status !== 404) {
|
||||||
|
throw new Error(`Unexpected response from blob check for layer ${layer.digest}: ${checkExistsResponse.status} ${checkExistsResponse.statusText}`);
|
||||||
|
}
|
||||||
|
core.info(`Uploading layer ${layer.digest}.`);
|
||||||
|
const initiateUploadResponse = await axios_1.default.post(uploadBlobEndpoint, layer, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${b64Token}`
|
||||||
|
},
|
||||||
|
validateStatus: () => {
|
||||||
|
return true; // Allow non 2xx responses
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (initiateUploadResponse.status !== 202) {
|
||||||
|
core.error(`Unexpected response from upload post ${uploadBlobEndpoint}: ${initiateUploadResponse.status}`);
|
||||||
|
throw new Error(`Unexpected response from POST upload ${initiateUploadResponse.status}`);
|
||||||
|
}
|
||||||
|
const locationResponseHeader = initiateUploadResponse.headers['location'];
|
||||||
|
if (locationResponseHeader === undefined) {
|
||||||
|
throw new Error(`No location header in response from upload post ${uploadBlobEndpoint} for layer ${layer.digest}`);
|
||||||
|
}
|
||||||
|
const pathname = `${locationResponseHeader}?digest=${layer.digest}`;
|
||||||
|
const uploadBlobUrl = new URL(pathname, registryURL).toString();
|
||||||
|
// TODO: must we handle the empty config layer? Maybe we can just skip calling this at all
|
||||||
|
let data;
|
||||||
|
if (file.size === 0) {
|
||||||
|
data = Buffer.alloc(0);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
data = fsHelper.readFileContents(file.path);
|
||||||
|
}
|
||||||
|
const putResponse = await axios_1.default.put(uploadBlobUrl, data, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${b64Token}`,
|
||||||
|
'Content-Type': 'application/octet-stream',
|
||||||
|
'Accept-Encoding': 'gzip', // TODO: What about for the config layer?
|
||||||
|
'Content-Length': layer.size.toString()
|
||||||
|
},
|
||||||
|
validateStatus: () => {
|
||||||
|
return true; // Allow non 2xx responses
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (putResponse.status !== 201) {
|
||||||
|
throw new Error(`Unexpected response from PUT upload ${putResponse.status} for layer ${layer.digest}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function uploadManifest(manifestJSON, manifestEndpoint, b64Token) {
|
||||||
|
core.info(`Uploading manifest to ${manifestEndpoint}.`);
|
||||||
|
const putResponse = await axios_1.default.put(manifestEndpoint, manifestJSON, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${b64Token}`,
|
||||||
|
'Content-Type': 'application/vnd.oci.image.manifest.v1+json'
|
||||||
|
},
|
||||||
|
validateStatus: () => {
|
||||||
|
return true; // Allow non 2xx responses
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (putResponse.status !== 201) {
|
||||||
|
throw new Error(`Unexpected response from PUT manifest ${putResponse.status}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function configureRequestDebugLogging() {
|
||||||
|
(0, axios_debug_log_1.default)({
|
||||||
|
request: (debug, config) => {
|
||||||
|
core.debug(`Request with ${config}`);
|
||||||
|
},
|
||||||
|
response: (debug, response) => {
|
||||||
|
core.debug(`Response with ${response}`);
|
||||||
|
},
|
||||||
|
error: (debug, error) => {
|
||||||
|
core.debug(`Error with ${error}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=ghcr-client.js.map
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"ghcr-client.js","sourceRoot":"","sources":["../src/ghcr-client.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,oDAAqC;AAGrC,kDAAyB;AACzB,sDAAuC;AACvC,sEAA2C;AAE3C,yEAAyE;AAClE,KAAK,UAAU,kBAAkB,CACtC,KAAa,EACb,QAAa,EACb,UAAkB,EAClB,SAAiB,EACjB,MAAc,EACd,OAAqB,EACrB,OAAqB,EACrB,QAA+B,EAC/B,aAAa,GAAG,KAAK;IAErB,IAAI,aAAa,EAAE,CAAC;QAClB,4BAA4B,EAAE,CAAA;IAChC,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAA;IAEtD,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAC/B,MAAM,UAAU,SAAS,EACzB,QAAQ,CACT,CAAC,QAAQ,EAAE,CAAA;IACZ,MAAM,kBAAkB,GAAG,IAAI,GAAG,CAChC,MAAM,UAAU,iBAAiB,EACjC,QAAQ,CACT,CAAC,QAAQ,EAAE,CAAA;IACZ,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAC9B,MAAM,UAAU,cAAc,MAAM,EAAE,EACtC,QAAQ,CACT,CAAC,QAAQ,EAAE,CAAA;IAEZ,IAAI,CAAC,IAAI,CACP,iDAAiD,MAAM,eAAe,OAAO,CAAC,IAAI,UAAU,OAAO,CAAC,IAAI,IAAI,CAC7G,CAAA;IAED,MAAM,YAAY,GAAoB,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,EAAC,KAAK,EAAC,EAAE;QACtE,QAAQ,KAAK,CAAC,SAAS,EAAE,CAAC;YACxB,KAAK,0DAA0D;gBAC7D,OAAO,WAAW,CAChB,KAAK,EACL,OAAO,EACP,QAAQ,EACR,iBAAiB,EACjB,kBAAkB,EAClB,QAAQ,CACT,CAAA;YACH,KAAK,qDAAqD;gBACxD,OAAO,WAAW,CAChB,KAAK,EACL,OAAO,EACP,QAAQ,EACR,iBAAiB,EACjB,kBAAkB,EAClB,QAAQ,CACT,CAAA;YACH,KAAK,uDAAuD;gBAC1D,OAAO,WAAW,CAChB,KAAK,EACL,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,EAC3C,QAAQ,EACR,iBAAiB,EACjB,kBAAkB,EAClB,QAAQ,CACT,CAAA;YACH;gBACE,MAAM,IAAI,KAAK,CAAC,sBAAsB,KAAK,CAAC,SAAS,EAAE,CAAC,CAAA;QAC5D,CAAC;IACH,CAAC,CAAC,CAAA;IAEF,MAAM,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAA;IAE/B,MAAM,cAAc,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAE,gBAAgB,EAAE,QAAQ,CAAC,CAAA;IAE1E,OAAO,IAAI,GAAG,CAAC,GAAG,UAAU,IAAI,MAAM,EAAE,EAAE,QAAQ,CAAC,CAAA;AACrD,CAAC;AAzED,gDAyEC;AAED,KAAK,UAAU,WAAW,CACxB,KAAyB,EACzB,IAAkB,EAClB,WAAgB,EAChB,iBAAyB,EACzB,kBAA0B,EAC1B,QAAgB;IAEhB,MAAM,mBAAmB,GAAG,MAAM,eAAK,CAAC,IAAI,CAC1C,iBAAiB,GAAG,KAAK,CAAC,MAAM,EAChC;QACE,OAAO,EAAE;YACP,aAAa,EAAE,UAAU,QAAQ,EAAE;SACpC;QACD,cAAc,EAAE,GAAG,EAAE;YACnB,OAAO,IAAI,CAAA,CAAC,0BAA0B;QACxC,CAAC;KACF,CACF,CAAA;IAED,IACE,mBAAmB,CAAC,MAAM,KAAK,GAAG;QAClC,mBAAmB,CAAC,MAAM,KAAK,GAAG,EAClC,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,SAAS,KAAK,CAAC,MAAM,mCAAmC,CAAC,CAAA;QACnE,OAAM;IACR,CAAC;IAED,IAAI,mBAAmB,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QACvC,MAAM,IAAI,KAAK,CACb,iDAAiD,KAAK,CAAC,MAAM,KAAK,mBAAmB,CAAC,MAAM,IAAI,mBAAmB,CAAC,UAAU,EAAE,CACjI,CAAA;IACH,CAAC;IAED,IAAI,CAAC,IAAI,CAAC,mBAAmB,KAAK,CAAC,MAAM,GAAG,CAAC,CAAA;IAE7C,MAAM,sBAAsB,GAAG,MAAM,eAAK,CAAC,IAAI,CAAC,kBAAkB,EAAE,KAAK,EAAE;QACzE,OAAO,EAAE;YACP,aAAa,EAAE,UAAU,QAAQ,EAAE;SACpC;QACD,cAAc,EAAE,GAAG,EAAE;YACnB,OAAO,IAAI,CAAA,CAAC,0BAA0B;QACxC,CAAC;KACF,CAAC,CAAA;IAEF,IAAI,sBAAsB,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC1C,IAAI,CAAC,KAAK,CACR,wCAAwC,kBAAkB,KAAK,sBAAsB,CAAC,MAAM,EAAE,CAC/F,CAAA;QACD,MAAM,IAAI,KAAK,CACb,wCAAwC,sBAAsB,CAAC,MAAM,EAAE,CACxE,CAAA;IACH,CAAC;IAED,MAAM,sBAAsB,GAAG,sBAAsB,CAAC,OAAO,CAAC,UAAU,CAAC,CAAA;IACzE,IAAI,sBAAsB,KAAK,SAAS,EAAE,CAAC;QACzC,MAAM,IAAI,KAAK,CACb,mDAAmD,kBAAkB,cAAc,KAAK,CAAC,MAAM,EAAE,CAClG,CAAA;IACH,CAAC;IAED,MAAM,QAAQ,GAAG,GAAG,sBAAsB,WAAW,KAAK,CAAC,MAAM,EAAE,CAAA;IACnE,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC,QAAQ,EAAE,CAAA;IAE/D,0FAA0F;IAC1F,IAAI,IAAY,CAAA;IAChB,IAAI,IAAI,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;QACpB,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;IACxB,CAAC;SAAM,CAAC;QACN,IAAI,GAAG,QAAQ,CAAC,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAC7C,CAAC;IAED,MAAM,WAAW,GAAG,MAAM,eAAK,CAAC,GAAG,CAAC,aAAa,EAAE,IAAI,EAAE;QACvD,OAAO,EAAE;YACP,aAAa,EAAE,UAAU,QAAQ,EAAE;YACnC,cAAc,EAAE,0BAA0B;YAC1C,iBAAiB,EAAE,MAAM,EAAE,yCAAyC;YACpE,gBAAgB,EAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,EAAE;SACxC;QACD,cAAc,EAAE,GAAG,EAAE;YACnB,OAAO,IAAI,CAAA,CAAC,0BAA0B;QACxC,CAAC;KACF,CAAC,CAAA;IAEF,IAAI,WAAW,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CACb,uCAAuC,WAAW,CAAC,MAAM,cAAc,KAAK,CAAC,MAAM,EAAE,CACtF,CAAA;IACH,CAAC;AACH,CAAC;AAED,KAAK,UAAU,cAAc,CAC3B,YAAoB,EACpB,gBAAwB,EACxB,QAAgB;IAEhB,IAAI,CAAC,IAAI,CAAC,yBAAyB,gBAAgB,GAAG,CAAC,CAAA;IAEvD,MAAM,WAAW,GAAG,MAAM,eAAK,CAAC,GAAG,CAAC,gBAAgB,EAAE,YAAY,EAAE;QAClE,OAAO,EAAE;YACP,aAAa,EAAE,UAAU,QAAQ,EAAE;YACnC,cAAc,EAAE,4CAA4C;SAC7D;QACD,cAAc,EAAE,GAAG,EAAE;YACnB,OAAO,IAAI,CAAA,CAAC,0BAA0B;QACxC,CAAC;KACF,CAAC,CAAA;IAEF,IAAI,WAAW,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CACb,yCAAyC,WAAW,CAAC,MAAM,EAAE,CAC9D,CAAA;IACH,CAAC;AACH,CAAC;AAED,SAAS,4BAA4B;IACnC,IAAA,yBAAa,EAAC;QACZ,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE;YACzB,IAAI,CAAC,KAAK,CAAC,gBAAgB,MAAM,EAAE,CAAC,CAAA;QACtC,CAAC;QACD,QAAQ,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE;YAC5B,IAAI,CAAC,KAAK,CAAC,iBAAiB,QAAQ,EAAE,CAAC,CAAA;QACzC,CAAC;QACD,KAAK,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;YACtB,IAAI,CAAC,KAAK,CAAC,cAAc,KAAK,EAAE,CAAC,CAAA;QACnC,CAAC;KACF,CAAC,CAAA;AACJ,CAAC"}
|
||||||
+357
-49
@@ -33737,6 +33737,277 @@ function populateMaps (extensions, types) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/***/ }),
|
||||||
|
|
||||||
|
/***/ 35871:
|
||||||
|
/***/ ((module) => {
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
|
||||||
|
function hasKey(obj, keys) {
|
||||||
|
var o = obj;
|
||||||
|
keys.slice(0, -1).forEach(function (key) {
|
||||||
|
o = o[key] || {};
|
||||||
|
});
|
||||||
|
|
||||||
|
var key = keys[keys.length - 1];
|
||||||
|
return key in o;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNumber(x) {
|
||||||
|
if (typeof x === 'number') { return true; }
|
||||||
|
if ((/^0x[0-9a-f]+$/i).test(x)) { return true; }
|
||||||
|
return (/^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(e[-+]?\d+)?$/).test(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isConstructorOrProto(obj, key) {
|
||||||
|
return (key === 'constructor' && typeof obj[key] === 'function') || key === '__proto__';
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = function (args, opts) {
|
||||||
|
if (!opts) { opts = {}; }
|
||||||
|
|
||||||
|
var flags = {
|
||||||
|
bools: {},
|
||||||
|
strings: {},
|
||||||
|
unknownFn: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof opts.unknown === 'function') {
|
||||||
|
flags.unknownFn = opts.unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof opts.boolean === 'boolean' && opts.boolean) {
|
||||||
|
flags.allBools = true;
|
||||||
|
} else {
|
||||||
|
[].concat(opts.boolean).filter(Boolean).forEach(function (key) {
|
||||||
|
flags.bools[key] = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var aliases = {};
|
||||||
|
|
||||||
|
function aliasIsBoolean(key) {
|
||||||
|
return aliases[key].some(function (x) {
|
||||||
|
return flags.bools[x];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(opts.alias || {}).forEach(function (key) {
|
||||||
|
aliases[key] = [].concat(opts.alias[key]);
|
||||||
|
aliases[key].forEach(function (x) {
|
||||||
|
aliases[x] = [key].concat(aliases[key].filter(function (y) {
|
||||||
|
return x !== y;
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
[].concat(opts.string).filter(Boolean).forEach(function (key) {
|
||||||
|
flags.strings[key] = true;
|
||||||
|
if (aliases[key]) {
|
||||||
|
[].concat(aliases[key]).forEach(function (k) {
|
||||||
|
flags.strings[k] = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var defaults = opts.default || {};
|
||||||
|
|
||||||
|
var argv = { _: [] };
|
||||||
|
|
||||||
|
function argDefined(key, arg) {
|
||||||
|
return (flags.allBools && (/^--[^=]+$/).test(arg))
|
||||||
|
|| flags.strings[key]
|
||||||
|
|| flags.bools[key]
|
||||||
|
|| aliases[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
function setKey(obj, keys, value) {
|
||||||
|
var o = obj;
|
||||||
|
for (var i = 0; i < keys.length - 1; i++) {
|
||||||
|
var key = keys[i];
|
||||||
|
if (isConstructorOrProto(o, key)) { return; }
|
||||||
|
if (o[key] === undefined) { o[key] = {}; }
|
||||||
|
if (
|
||||||
|
o[key] === Object.prototype
|
||||||
|
|| o[key] === Number.prototype
|
||||||
|
|| o[key] === String.prototype
|
||||||
|
) {
|
||||||
|
o[key] = {};
|
||||||
|
}
|
||||||
|
if (o[key] === Array.prototype) { o[key] = []; }
|
||||||
|
o = o[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastKey = keys[keys.length - 1];
|
||||||
|
if (isConstructorOrProto(o, lastKey)) { return; }
|
||||||
|
if (
|
||||||
|
o === Object.prototype
|
||||||
|
|| o === Number.prototype
|
||||||
|
|| o === String.prototype
|
||||||
|
) {
|
||||||
|
o = {};
|
||||||
|
}
|
||||||
|
if (o === Array.prototype) { o = []; }
|
||||||
|
if (o[lastKey] === undefined || flags.bools[lastKey] || typeof o[lastKey] === 'boolean') {
|
||||||
|
o[lastKey] = value;
|
||||||
|
} else if (Array.isArray(o[lastKey])) {
|
||||||
|
o[lastKey].push(value);
|
||||||
|
} else {
|
||||||
|
o[lastKey] = [o[lastKey], value];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setArg(key, val, arg) {
|
||||||
|
if (arg && flags.unknownFn && !argDefined(key, arg)) {
|
||||||
|
if (flags.unknownFn(arg) === false) { return; }
|
||||||
|
}
|
||||||
|
|
||||||
|
var value = !flags.strings[key] && isNumber(val)
|
||||||
|
? Number(val)
|
||||||
|
: val;
|
||||||
|
setKey(argv, key.split('.'), value);
|
||||||
|
|
||||||
|
(aliases[key] || []).forEach(function (x) {
|
||||||
|
setKey(argv, x.split('.'), value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(flags.bools).forEach(function (key) {
|
||||||
|
setArg(key, defaults[key] === undefined ? false : defaults[key]);
|
||||||
|
});
|
||||||
|
|
||||||
|
var notFlags = [];
|
||||||
|
|
||||||
|
if (args.indexOf('--') !== -1) {
|
||||||
|
notFlags = args.slice(args.indexOf('--') + 1);
|
||||||
|
args = args.slice(0, args.indexOf('--'));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < args.length; i++) {
|
||||||
|
var arg = args[i];
|
||||||
|
var key;
|
||||||
|
var next;
|
||||||
|
|
||||||
|
if ((/^--.+=/).test(arg)) {
|
||||||
|
// Using [\s\S] instead of . because js doesn't support the
|
||||||
|
// 'dotall' regex modifier. See:
|
||||||
|
// http://stackoverflow.com/a/1068308/13216
|
||||||
|
var m = arg.match(/^--([^=]+)=([\s\S]*)$/);
|
||||||
|
key = m[1];
|
||||||
|
var value = m[2];
|
||||||
|
if (flags.bools[key]) {
|
||||||
|
value = value !== 'false';
|
||||||
|
}
|
||||||
|
setArg(key, value, arg);
|
||||||
|
} else if ((/^--no-.+/).test(arg)) {
|
||||||
|
key = arg.match(/^--no-(.+)/)[1];
|
||||||
|
setArg(key, false, arg);
|
||||||
|
} else if ((/^--.+/).test(arg)) {
|
||||||
|
key = arg.match(/^--(.+)/)[1];
|
||||||
|
next = args[i + 1];
|
||||||
|
if (
|
||||||
|
next !== undefined
|
||||||
|
&& !(/^(-|--)[^-]/).test(next)
|
||||||
|
&& !flags.bools[key]
|
||||||
|
&& !flags.allBools
|
||||||
|
&& (aliases[key] ? !aliasIsBoolean(key) : true)
|
||||||
|
) {
|
||||||
|
setArg(key, next, arg);
|
||||||
|
i += 1;
|
||||||
|
} else if ((/^(true|false)$/).test(next)) {
|
||||||
|
setArg(key, next === 'true', arg);
|
||||||
|
i += 1;
|
||||||
|
} else {
|
||||||
|
setArg(key, flags.strings[key] ? '' : true, arg);
|
||||||
|
}
|
||||||
|
} else if ((/^-[^-]+/).test(arg)) {
|
||||||
|
var letters = arg.slice(1, -1).split('');
|
||||||
|
|
||||||
|
var broken = false;
|
||||||
|
for (var j = 0; j < letters.length; j++) {
|
||||||
|
next = arg.slice(j + 2);
|
||||||
|
|
||||||
|
if (next === '-') {
|
||||||
|
setArg(letters[j], next, arg);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((/[A-Za-z]/).test(letters[j]) && next[0] === '=') {
|
||||||
|
setArg(letters[j], next.slice(1), arg);
|
||||||
|
broken = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(/[A-Za-z]/).test(letters[j])
|
||||||
|
&& (/-?\d+(\.\d*)?(e-?\d+)?$/).test(next)
|
||||||
|
) {
|
||||||
|
setArg(letters[j], next, arg);
|
||||||
|
broken = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (letters[j + 1] && letters[j + 1].match(/\W/)) {
|
||||||
|
setArg(letters[j], arg.slice(j + 2), arg);
|
||||||
|
broken = true;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
setArg(letters[j], flags.strings[letters[j]] ? '' : true, arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
key = arg.slice(-1)[0];
|
||||||
|
if (!broken && key !== '-') {
|
||||||
|
if (
|
||||||
|
args[i + 1]
|
||||||
|
&& !(/^(-|--)[^-]/).test(args[i + 1])
|
||||||
|
&& !flags.bools[key]
|
||||||
|
&& (aliases[key] ? !aliasIsBoolean(key) : true)
|
||||||
|
) {
|
||||||
|
setArg(key, args[i + 1], arg);
|
||||||
|
i += 1;
|
||||||
|
} else if (args[i + 1] && (/^(true|false)$/).test(args[i + 1])) {
|
||||||
|
setArg(key, args[i + 1] === 'true', arg);
|
||||||
|
i += 1;
|
||||||
|
} else {
|
||||||
|
setArg(key, flags.strings[key] ? '' : true, arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!flags.unknownFn || flags.unknownFn(arg) !== false) {
|
||||||
|
argv._.push(flags.strings._ || !isNumber(arg) ? arg : Number(arg));
|
||||||
|
}
|
||||||
|
if (opts.stopEarly) {
|
||||||
|
argv._.push.apply(argv._, args.slice(i + 1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(defaults).forEach(function (k) {
|
||||||
|
if (!hasKey(argv, k.split('.'))) {
|
||||||
|
setKey(argv, k.split('.'), defaults[k]);
|
||||||
|
|
||||||
|
(aliases[k] || []).forEach(function (x) {
|
||||||
|
setKey(argv, x.split('.'), defaults[k]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (opts['--']) {
|
||||||
|
argv['--'] = notFlags.slice();
|
||||||
|
} else {
|
||||||
|
notFlags.forEach(function (k) {
|
||||||
|
argv._.push(k);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return argv;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
/***/ }),
|
/***/ }),
|
||||||
|
|
||||||
/***/ 41077:
|
/***/ 41077:
|
||||||
@@ -74422,7 +74693,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|||||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
};
|
};
|
||||||
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||||
exports.bundleFilesintoDirectory = exports.readFileContents = exports.isDirectory = exports.createArchives = exports.removeDir = exports.createTempDir = void 0;
|
exports.readFileContents = exports.isActionRepo = exports.isDirectory = exports.createArchives = exports.getConsolidatedDirectory = exports.removeDir = exports.createTempDir = void 0;
|
||||||
const fs = __importStar(__nccwpck_require__(57147));
|
const fs = __importStar(__nccwpck_require__(57147));
|
||||||
const fs_extra_1 = __importDefault(__nccwpck_require__(5630));
|
const fs_extra_1 = __importDefault(__nccwpck_require__(5630));
|
||||||
const path = __importStar(__nccwpck_require__(71017));
|
const path = __importStar(__nccwpck_require__(71017));
|
||||||
@@ -74445,6 +74716,24 @@ function removeDir(dir) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
exports.removeDir = removeDir;
|
exports.removeDir = removeDir;
|
||||||
|
// TODO: rename this function, it is not state-preserving, so it shouldn't just be called "get'"
|
||||||
|
function getConsolidatedDirectory(filePathSpec) {
|
||||||
|
const paths = filePathSpec.split(' '); // TODO: handle files with spaces
|
||||||
|
// TODO: do check on paths to make sure they're valid and not reaching outside the space
|
||||||
|
let consolidatedPath = '';
|
||||||
|
let needToCleanUpDir = false;
|
||||||
|
if (paths.length === 1 && isDirectory(paths[0])) {
|
||||||
|
// If the path is a single directory, we can skip the bundling step
|
||||||
|
consolidatedPath = paths[0];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Otherwise, we need to bundle the files & folders into a temporary directory
|
||||||
|
consolidatedPath = bundleFilesintoDirectory(paths);
|
||||||
|
needToCleanUpDir = true;
|
||||||
|
}
|
||||||
|
return { consolidatedPath, needToCleanUpDir };
|
||||||
|
}
|
||||||
|
exports.getConsolidatedDirectory = getConsolidatedDirectory;
|
||||||
// Creates both a tar.gz and zip archive of the given directory and returns the paths to both archives (stored in the provided target directory)
|
// Creates both a tar.gz and zip archive of the given directory and returns the paths to both archives (stored in the provided target directory)
|
||||||
// as well as the size/sha256 hash of each file.
|
// as well as the size/sha256 hash of each file.
|
||||||
async function createArchives(distPath, archiveTargetPath = createTempDir()) {
|
async function createArchives(distPath, archiveTargetPath = createTempDir()) {
|
||||||
@@ -74463,7 +74752,7 @@ async function createArchives(distPath, archiveTargetPath = createTempDir()) {
|
|||||||
resolve(fileMetadata(zipPath));
|
resolve(fileMetadata(zipPath));
|
||||||
});
|
});
|
||||||
archive.pipe(output);
|
archive.pipe(output);
|
||||||
archive.directory(distPath, false);
|
archive.directory(distPath, false); // TODO: make sure this doesn't include dirs that start with ., same with below
|
||||||
archive.finalize();
|
archive.finalize();
|
||||||
});
|
});
|
||||||
const createTarPromise = new Promise((resolve, reject) => {
|
const createTarPromise = new Promise((resolve, reject) => {
|
||||||
@@ -74493,27 +74782,32 @@ function isDirectory(dirPath) {
|
|||||||
return fs.existsSync(dirPath) && fs.lstatSync(dirPath).isDirectory();
|
return fs.existsSync(dirPath) && fs.lstatSync(dirPath).isDirectory();
|
||||||
}
|
}
|
||||||
exports.isDirectory = isDirectory;
|
exports.isDirectory = isDirectory;
|
||||||
|
function isActionRepo(stagingDir) {
|
||||||
|
return (fs.existsSync(path.join(stagingDir, 'action.yml')) ||
|
||||||
|
fs.existsSync(path.join(stagingDir, 'action.yaml')));
|
||||||
|
}
|
||||||
|
exports.isActionRepo = isActionRepo;
|
||||||
function readFileContents(filePath) {
|
function readFileContents(filePath) {
|
||||||
return fs.readFileSync(filePath);
|
return fs.readFileSync(filePath);
|
||||||
}
|
}
|
||||||
exports.readFileContents = readFileContents;
|
exports.readFileContents = readFileContents;
|
||||||
function bundleFilesintoDirectory(files, targetDir = createTempDir()) {
|
function bundleFilesintoDirectory(filePaths) {
|
||||||
for (const file of files) {
|
const targetDir = createTempDir();
|
||||||
if (!fs.existsSync(file)) {
|
for (const filePath of filePaths) {
|
||||||
throw new Error(`File ${file} does not exist`);
|
if (!fs.existsSync(filePath)) {
|
||||||
|
throw new Error(`filePath ${filePath} does not exist`);
|
||||||
}
|
}
|
||||||
if (isDirectory(file)) {
|
if (isDirectory(filePath)) {
|
||||||
const targetFolder = path.join(targetDir, path.basename(file));
|
const targetFolder = path.join(targetDir, path.basename(filePath)); // TODO: basename is probably not what we actually want here. Or is it? Maybe conflicts between dir1/dir2 and dir1/dir3/dir2 are just user error or ??
|
||||||
fs_extra_1.default.copySync(file, targetFolder);
|
fs_extra_1.default.copySync(filePath, targetFolder); // TODO: ignore files preceded by .
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const targetFile = path.join(targetDir, path.basename(file));
|
const targetFile = path.join(targetDir, path.basename(filePath));
|
||||||
fs.copyFileSync(file, targetFile);
|
fs.copyFileSync(filePath, targetFile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return targetDir;
|
return targetDir;
|
||||||
}
|
}
|
||||||
exports.bundleFilesintoDirectory = bundleFilesintoDirectory;
|
|
||||||
// Converts a file path to a filemetadata object by querying the fs for relevant metadata.
|
// Converts a file path to a filemetadata object by querying the fs for relevant metadata.
|
||||||
async function fileMetadata(filePath) {
|
async function fileMetadata(filePath) {
|
||||||
const stats = fs.statSync(filePath);
|
const stats = fs.statSync(filePath);
|
||||||
@@ -74694,6 +74988,27 @@ function configureRequestDebugLogging() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/***/ }),
|
||||||
|
|
||||||
|
/***/ 6144:
|
||||||
|
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||||
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||||
|
/**
|
||||||
|
* The entrypoint for the action.
|
||||||
|
*/
|
||||||
|
const main_1 = __nccwpck_require__(70399);
|
||||||
|
const minimist_1 = __importDefault(__nccwpck_require__(35871));
|
||||||
|
const path = (0, minimist_1.default)(process.argv.slice(2)).path || '.';
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
(0, main_1.run)(path);
|
||||||
|
|
||||||
|
|
||||||
/***/ }),
|
/***/ }),
|
||||||
|
|
||||||
/***/ 70399:
|
/***/ 70399:
|
||||||
@@ -74735,11 +75050,12 @@ const fsHelper = __importStar(__nccwpck_require__(76642));
|
|||||||
const ociContainer = __importStar(__nccwpck_require__(33207));
|
const ociContainer = __importStar(__nccwpck_require__(33207));
|
||||||
const ghcr = __importStar(__nccwpck_require__(62894));
|
const ghcr = __importStar(__nccwpck_require__(62894));
|
||||||
const semver_1 = __importDefault(__nccwpck_require__(11383));
|
const semver_1 = __importDefault(__nccwpck_require__(11383));
|
||||||
|
const crypto_1 = __importDefault(__nccwpck_require__(6113));
|
||||||
/**
|
/**
|
||||||
* The main function for the action.
|
* The main function for the action.
|
||||||
* @returns {Promise<void>} Resolves when the action is complete.
|
* @returns {Promise<void>} Resolves when the action is complete.
|
||||||
*/
|
*/
|
||||||
async function run() {
|
async function run(pathInput) {
|
||||||
const tmpDirs = [];
|
const tmpDirs = [];
|
||||||
try {
|
try {
|
||||||
// Parse and validate Actions execution context, including the repository name, release name and event type
|
// Parse and validate Actions execution context, including the repository name, release name and event type
|
||||||
@@ -74758,37 +75074,39 @@ async function run() {
|
|||||||
// https://docs.github.com/en/actions/creating-actions/releasing-and-maintaining-actions
|
// https://docs.github.com/en/actions/creating-actions/releasing-and-maintaining-actions
|
||||||
const targetVersion = semver_1.default.parse(releaseTag.replace(/^v/, ''));
|
const targetVersion = semver_1.default.parse(releaseTag.replace(/^v/, ''));
|
||||||
if (!targetVersion) {
|
if (!targetVersion) {
|
||||||
// TODO: We may want to limit semvers to only x.x.x, without the pre-release tags, but for now we'll allow them.
|
|
||||||
core.setFailed(`${releaseTag} is not a valid semantic version, and so cannot be uploaded as an Immutable Action.`);
|
core.setFailed(`${releaseTag} is not a valid semantic version, and so cannot be uploaded as an Immutable Action.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Gather & validate user inputs
|
const token = process.env.TOKEN;
|
||||||
const token = core.getInput('token');
|
const { consolidatedPath, needToCleanUpDir } = fsHelper.getConsolidatedDirectory(pathInput);
|
||||||
const registryURL = new URL(core.getInput('registry')); // TODO: Should this be dynamic? Maybe an API endpoint to grab the registry for GHES/proxima purposes.
|
if (needToCleanUpDir) {
|
||||||
// Paths to be included in the OCI image
|
tmpDirs.push(consolidatedPath);
|
||||||
const paths = core.getInput('path').split(' ');
|
|
||||||
let path = '';
|
|
||||||
if (paths.length === 1 && fsHelper.isDirectory(paths[0])) {
|
|
||||||
// If the path is a single directory, we can skip the bundling step
|
|
||||||
path = paths[0];
|
|
||||||
}
|
}
|
||||||
else {
|
if (!fsHelper.isActionRepo(consolidatedPath)) {
|
||||||
// Otherwise, we need to bundle the files & folders into a temporary directory
|
core.setFailed('action.y(a)ml not found. Action packages can be created only for action repositories.');
|
||||||
const bundleDir = fsHelper.createTempDir();
|
return;
|
||||||
tmpDirs.push(bundleDir);
|
|
||||||
path = fsHelper.bundleFilesintoDirectory(paths, bundleDir);
|
|
||||||
}
|
}
|
||||||
// Create a temporary directory to store the archives
|
// Create a temporary directory to store the archives
|
||||||
const archiveDir = fsHelper.createTempDir();
|
const archiveDir = fsHelper.createTempDir();
|
||||||
tmpDirs.push(archiveDir);
|
tmpDirs.push(archiveDir);
|
||||||
const archives = await fsHelper.createArchives(path, archiveDir);
|
const archives = await fsHelper.createArchives(consolidatedPath, archiveDir);
|
||||||
const manifest = ociContainer.createActionPackageManifest(archives.tarFile, archives.zipFile, repository, targetVersion.raw, new Date());
|
const manifest = ociContainer.createActionPackageManifest(archives.tarFile, archives.zipFile, repository, targetVersion.raw, new Date());
|
||||||
|
// Generate SHA-256 hash of the manifest
|
||||||
|
const manifestSHA = crypto_1.default.createHash('sha256');
|
||||||
|
const manifestHash = manifestSHA
|
||||||
|
.update(JSON.stringify(manifest))
|
||||||
|
.digest('hex');
|
||||||
|
const response = await fetch(`${process.env.GITHUB_API_URL}/packages/container-registry-url`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch status page: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
const registryURL = new URL(data.url);
|
||||||
|
console.log(`Container registry URL: ${registryURL}`);
|
||||||
const packageURL = await ghcr.publishOCIArtifact(token, registryURL, repository, releaseId.toString(), targetVersion.raw, archives.zipFile, archives.tarFile, manifest, true);
|
const packageURL = await ghcr.publishOCIArtifact(token, registryURL, repository, releaseId.toString(), targetVersion.raw, archives.zipFile, archives.tarFile, manifest, true);
|
||||||
core.setOutput('package-url', packageURL.toString());
|
core.setOutput('package-url', packageURL.toString());
|
||||||
// TODO: We might need to do some attestation stuff here, but unsure how to integrate it yet.
|
|
||||||
// We might need to return the manifest JSON from the Action and link it to another action,
|
|
||||||
// or we might be able to make an API call here. It's unclear at this point.
|
|
||||||
core.setOutput('package-manifest', JSON.stringify(manifest));
|
core.setOutput('package-manifest', JSON.stringify(manifest));
|
||||||
|
core.setOutput('package-manifest-sha', `sha256:${manifestHash}`);
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
// Fail the workflow run if an error occurs
|
// Fail the workflow run if an error occurs
|
||||||
@@ -79522,22 +79840,12 @@ module.exports = JSON.parse('{"application/1d-interleaved-parityfec":{"source":"
|
|||||||
/******/ if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = __dirname + "/";
|
/******/ if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = __dirname + "/";
|
||||||
/******/
|
/******/
|
||||||
/************************************************************************/
|
/************************************************************************/
|
||||||
var __webpack_exports__ = {};
|
/******/
|
||||||
// This entry need to be wrapped in an IIFE because it need to be in strict mode.
|
/******/ // startup
|
||||||
(() => {
|
/******/ // Load entry module and return exports
|
||||||
"use strict";
|
/******/ // This entry module is referenced by other modules so it can't be inlined
|
||||||
var exports = __webpack_exports__;
|
/******/ var __webpack_exports__ = __nccwpck_require__(6144);
|
||||||
|
/******/ module.exports = __webpack_exports__;
|
||||||
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
/******/
|
||||||
/**
|
|
||||||
* The entrypoint for the action.
|
|
||||||
*/
|
|
||||||
const main_1 = __nccwpck_require__(70399);
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
(0, main_1.run)();
|
|
||||||
|
|
||||||
})();
|
|
||||||
|
|
||||||
module.exports = __webpack_exports__;
|
|
||||||
/******/ })()
|
/******/ })()
|
||||||
;
|
;
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;AAAA;;GAEG;AACH,iCAA4B;AAE5B,mEAAmE;AACnE,IAAA,UAAG,GAAE,CAAA"}
|
||||||
+22
@@ -1528,6 +1528,28 @@ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
|||||||
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
|
||||||
|
minimist
|
||||||
|
MIT
|
||||||
|
This software is released under the MIT license:
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
|
||||||
minipass
|
minipass
|
||||||
ISC
|
ISC
|
||||||
The ISC License
|
The ISC License
|
||||||
|
|||||||
+108
@@ -0,0 +1,108 @@
|
|||||||
|
"use strict";
|
||||||
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||||
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||||
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||||
|
}
|
||||||
|
Object.defineProperty(o, k2, desc);
|
||||||
|
}) : (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
o[k2] = m[k];
|
||||||
|
}));
|
||||||
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||||
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||||
|
}) : function(o, v) {
|
||||||
|
o["default"] = v;
|
||||||
|
});
|
||||||
|
var __importStar = (this && this.__importStar) || function (mod) {
|
||||||
|
if (mod && mod.__esModule) return mod;
|
||||||
|
var result = {};
|
||||||
|
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||||
|
__setModuleDefault(result, mod);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||||
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.run = void 0;
|
||||||
|
const core = __importStar(require("@actions/core"));
|
||||||
|
const github = __importStar(require("@actions/github"));
|
||||||
|
const fsHelper = __importStar(require("./fs-helper"));
|
||||||
|
const ociContainer = __importStar(require("./oci-container"));
|
||||||
|
const ghcr = __importStar(require("./ghcr-client"));
|
||||||
|
const semver_1 = __importDefault(require("semver"));
|
||||||
|
/**
|
||||||
|
* The main function for the action.
|
||||||
|
* @returns {Promise<void>} Resolves when the action is complete.
|
||||||
|
*/
|
||||||
|
async function run() {
|
||||||
|
const tmpDirs = [];
|
||||||
|
try {
|
||||||
|
// Parse and validate Actions execution context, including the repository name, release name and event type
|
||||||
|
const repository = process.env.GITHUB_REPOSITORY || '';
|
||||||
|
if (repository === '') {
|
||||||
|
core.setFailed(`Could not find Repository.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (github.context.eventName !== 'release') {
|
||||||
|
core.setFailed('Please ensure you have the workflow trigger as release.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const releaseId = github.context.payload.release.id;
|
||||||
|
const releaseTag = github.context.payload.release.tag_name;
|
||||||
|
// Strip any leading 'v' from the tag in case the release format is e.g. 'v1.0.0' as recommended by GitHub docs
|
||||||
|
// https://docs.github.com/en/actions/creating-actions/releasing-and-maintaining-actions
|
||||||
|
const targetVersion = semver_1.default.parse(releaseTag.replace(/^v/, ''));
|
||||||
|
if (!targetVersion) {
|
||||||
|
// TODO: We may want to limit semvers to only x.x.x, without the pre-release tags, but for now we'll allow them.
|
||||||
|
core.setFailed(`${releaseTag} is not a valid semantic version, and so cannot be uploaded as an Immutable Action.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Gather & validate user inputs
|
||||||
|
const token = core.getInput('token');
|
||||||
|
const registryURL = new URL('https://ghcr.io/'); // TODO: Should this be dynamic? Maybe an API endpoint to grab the registry for GHES/proxima purposes.
|
||||||
|
console.log(core.getInput('registry'));
|
||||||
|
console.log(`registryURL: ${registryURL}`);
|
||||||
|
// Paths to be included in the OCI image
|
||||||
|
const paths = core.getInput('path').split(' ');
|
||||||
|
let path = '';
|
||||||
|
if (paths.length === 1 && fsHelper.isDirectory(paths[0])) {
|
||||||
|
// If the path is a single directory, we can skip the bundling step
|
||||||
|
path = paths[0];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Otherwise, we need to bundle the files & folders into a temporary directory
|
||||||
|
const bundleDir = fsHelper.createTempDir();
|
||||||
|
tmpDirs.push(bundleDir);
|
||||||
|
path = fsHelper.bundleFilesintoDirectory(paths, bundleDir);
|
||||||
|
}
|
||||||
|
// Create a temporary directory to store the archives
|
||||||
|
const archiveDir = fsHelper.createTempDir();
|
||||||
|
tmpDirs.push(archiveDir);
|
||||||
|
const archives = await fsHelper.createArchives(path, archiveDir);
|
||||||
|
const manifest = ociContainer.createActionPackageManifest(archives.tarFile, archives.zipFile, repository, targetVersion.raw, new Date());
|
||||||
|
const packageURL = await ghcr.publishOCIArtifact(token, registryURL, repository, releaseId.toString(), targetVersion.raw, archives.zipFile, archives.tarFile, manifest, true);
|
||||||
|
core.setOutput('package-url', packageURL.toString());
|
||||||
|
// TODO: We might need to do some attestation stuff here, but unsure how to integrate it yet.
|
||||||
|
// We might need to return the manifest JSON from the Action and link it to another action,
|
||||||
|
// or we might be able to make an API call here. It's unclear at this point.
|
||||||
|
core.setOutput('package-manifest', JSON.stringify(manifest));
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
// Fail the workflow run if an error occurs
|
||||||
|
if (error instanceof Error)
|
||||||
|
core.setFailed(error.message);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
// Clean up any temporary directories that exist
|
||||||
|
for (const tmpDir of tmpDirs) {
|
||||||
|
if (tmpDir !== '') {
|
||||||
|
fsHelper.removeDir(tmpDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.run = run;
|
||||||
|
//# sourceMappingURL=main.js.map
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"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"}
|
||||||
+65
@@ -0,0 +1,65 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.createActionPackageManifest = void 0;
|
||||||
|
// Given a name and archive metadata, creates a manifest in the format expected by GHCR for an Actions Package.
|
||||||
|
function createActionPackageManifest(tarFile, zipFile, repository, version, created) {
|
||||||
|
const configLayer = createConfigLayer();
|
||||||
|
const sanitizedRepo = sanitizeRepository(repository);
|
||||||
|
const tarLayer = createTarLayer(tarFile, sanitizedRepo, version);
|
||||||
|
const zipLayer = createZipLayer(zipFile, sanitizedRepo, version);
|
||||||
|
const manifest = {
|
||||||
|
schemaVersion: 2,
|
||||||
|
mediaType: 'application/vnd.oci.image.manifest.v1+json',
|
||||||
|
artifactType: 'application/vnd.oci.image.manifest.v1+json',
|
||||||
|
config: configLayer,
|
||||||
|
layers: [configLayer, tarLayer, zipLayer],
|
||||||
|
annotations: {
|
||||||
|
'org.opencontainers.image.created': created.toISOString(),
|
||||||
|
'action.tar.gz.digest': tarFile.sha256,
|
||||||
|
'action.zip.digest': zipFile.sha256,
|
||||||
|
'com.github.package.type': 'actions_oci_pkg'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return manifest;
|
||||||
|
}
|
||||||
|
exports.createActionPackageManifest = createActionPackageManifest;
|
||||||
|
// TODO: is this ok hardcoded?
|
||||||
|
function createConfigLayer() {
|
||||||
|
const configLayer = {
|
||||||
|
mediaType: 'application/vnd.github.actions.package.config.v1+json',
|
||||||
|
size: 0,
|
||||||
|
digest: 'sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
|
||||||
|
annotations: {
|
||||||
|
'org.opencontainers.image.title': 'config.json'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return configLayer;
|
||||||
|
}
|
||||||
|
function createZipLayer(zipFile, repository, version) {
|
||||||
|
const zipLayer = {
|
||||||
|
mediaType: 'application/vnd.github.actions.package.layer.v1.zip',
|
||||||
|
size: zipFile.size,
|
||||||
|
digest: zipFile.sha256,
|
||||||
|
annotations: {
|
||||||
|
'org.opencontainers.image.title': `${repository}_${version}.zip`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return zipLayer;
|
||||||
|
}
|
||||||
|
function createTarLayer(tarFile, repository, version) {
|
||||||
|
const tarLayer = {
|
||||||
|
mediaType: 'application/vnd.github.actions.package.layer.v1.tar+gzip',
|
||||||
|
size: tarFile.size,
|
||||||
|
digest: tarFile.sha256,
|
||||||
|
annotations: {
|
||||||
|
'org.opencontainers.image.title': `${repository}_${version}.tar.gz`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return tarLayer;
|
||||||
|
}
|
||||||
|
// Remove slashes so we can use the repository in a filename
|
||||||
|
// repository usually includes the namespace too, e.g. my-org/my-repo
|
||||||
|
function sanitizeRepository(repository) {
|
||||||
|
return repository.replace('/', '-');
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=oci-container.js.map
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"oci-container.js","sourceRoot":"","sources":["../src/oci-container.ts"],"names":[],"mappings":";;;AAkBA,+GAA+G;AAC/G,SAAgB,2BAA2B,CACzC,OAAqB,EACrB,OAAqB,EACrB,UAAkB,EAClB,OAAe,EACf,OAAa;IAEb,MAAM,WAAW,GAAG,iBAAiB,EAAE,CAAA;IACvC,MAAM,aAAa,GAAG,kBAAkB,CAAC,UAAU,CAAC,CAAA;IACpD,MAAM,QAAQ,GAAG,cAAc,CAAC,OAAO,EAAE,aAAa,EAAE,OAAO,CAAC,CAAA;IAChE,MAAM,QAAQ,GAAG,cAAc,CAAC,OAAO,EAAE,aAAa,EAAE,OAAO,CAAC,CAAA;IAEhE,MAAM,QAAQ,GAAa;QACzB,aAAa,EAAE,CAAC;QAChB,SAAS,EAAE,4CAA4C;QACvD,YAAY,EAAE,4CAA4C;QAC1D,MAAM,EAAE,WAAW;QACnB,MAAM,EAAE,CAAC,WAAW,EAAE,QAAQ,EAAE,QAAQ,CAAC;QACzC,WAAW,EAAE;YACX,kCAAkC,EAAE,OAAO,CAAC,WAAW,EAAE;YACzD,sBAAsB,EAAE,OAAO,CAAC,MAAM;YACtC,mBAAmB,EAAE,OAAO,CAAC,MAAM;YACnC,yBAAyB,EAAE,iBAAiB;SAC7C;KACF,CAAA;IAED,OAAO,QAAQ,CAAA;AACjB,CAAC;AA3BD,kEA2BC;AAED,8BAA8B;AAC9B,SAAS,iBAAiB;IACxB,MAAM,WAAW,GAAU;QACzB,SAAS,EAAE,uDAAuD;QAClE,IAAI,EAAE,CAAC;QACP,MAAM,EACJ,yEAAyE;QAC3E,WAAW,EAAE;YACX,gCAAgC,EAAE,aAAa;SAChD;KACF,CAAA;IAED,OAAO,WAAW,CAAA;AACpB,CAAC;AAED,SAAS,cAAc,CACrB,OAAqB,EACrB,UAAkB,EAClB,OAAe;IAEf,MAAM,QAAQ,GAAU;QACtB,SAAS,EAAE,qDAAqD;QAChE,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,WAAW,EAAE;YACX,gCAAgC,EAAE,GAAG,UAAU,IAAI,OAAO,MAAM;SACjE;KACF,CAAA;IAED,OAAO,QAAQ,CAAA;AACjB,CAAC;AAED,SAAS,cAAc,CACrB,OAAqB,EACrB,UAAkB,EAClB,OAAe;IAEf,MAAM,QAAQ,GAAU;QACtB,SAAS,EAAE,0DAA0D;QACrE,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,WAAW,EAAE;YACX,gCAAgC,EAAE,GAAG,UAAU,IAAI,OAAO,SAAS;SACpE;KACF,CAAA;IAED,OAAO,QAAQ,CAAA;AACjB,CAAC;AAED,4DAA4D;AAC5D,qEAAqE;AACrE,SAAS,kBAAkB,CAAC,UAAkB;IAC5C,OAAO,UAAU,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;AACrC,CAAC"}
|
||||||
Executable
+27
@@ -0,0 +1,27 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
VERSION=$1
|
||||||
|
MESSAGE=$2
|
||||||
|
|
||||||
|
if [ -z "$VERSION" ]
|
||||||
|
then
|
||||||
|
echo "No version supplied"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$MESSAGE" ]
|
||||||
|
then
|
||||||
|
echo "No message supplied"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Generating new version $VERSION with message $MESSAGE"
|
||||||
|
|
||||||
|
#sed -i '' -E 's/ddivad195\/publish-action-package\/package-and-publish.*$/ddivad195\/publish-action-package\/package-and-publish@'$VERSION'/g' action.yml
|
||||||
|
npm run bundle
|
||||||
|
git add .
|
||||||
|
git commit -m "$VERSION: $MESSAGE"
|
||||||
|
git push
|
||||||
|
git tag $VERSION
|
||||||
|
git push origin $VERSION
|
||||||
|
gh release create --repo ddivad195/publish-action-package --title $VERSION --notes $VERSION $VERSION
|
||||||
Generated
+8
-1
@@ -17,12 +17,14 @@
|
|||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
"axios-debug-log": "^1.0.0",
|
"axios-debug-log": "^1.0.0",
|
||||||
"fs-extra": "^11.1.1",
|
"fs-extra": "^11.1.1",
|
||||||
|
"minimist": "^1.2.8",
|
||||||
"tar": "^6.2.0"
|
"tar": "^6.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/archiver": "^6.0.2",
|
"@types/archiver": "^6.0.2",
|
||||||
"@types/axios": "^0.14.0",
|
"@types/axios": "^0.14.0",
|
||||||
"@types/jest": "^29.5.11",
|
"@types/jest": "^29.5.11",
|
||||||
|
"@types/minimist": "^1.2.5",
|
||||||
"@types/node": "^20.11.4",
|
"@types/node": "^20.11.4",
|
||||||
"@types/tar": "^6.1.10",
|
"@types/tar": "^6.1.10",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.12.0",
|
"@typescript-eslint/eslint-plugin": "^6.12.0",
|
||||||
@@ -1672,6 +1674,12 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/minimist": {
|
||||||
|
"version": "1.2.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz",
|
||||||
|
"integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/ms": {
|
"node_modules/@types/ms": {
|
||||||
"version": "0.7.34",
|
"version": "0.7.34",
|
||||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz",
|
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz",
|
||||||
@@ -6157,7 +6165,6 @@
|
|||||||
"version": "1.2.8",
|
"version": "1.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||||
"dev": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,12 +74,14 @@
|
|||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
"axios-debug-log": "^1.0.0",
|
"axios-debug-log": "^1.0.0",
|
||||||
"fs-extra": "^11.1.1",
|
"fs-extra": "^11.1.1",
|
||||||
|
"minimist": "^1.2.8",
|
||||||
"tar": "^6.2.0"
|
"tar": "^6.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/archiver": "^6.0.2",
|
"@types/archiver": "^6.0.2",
|
||||||
"@types/axios": "^0.14.0",
|
"@types/axios": "^0.14.0",
|
||||||
"@types/jest": "^29.5.11",
|
"@types/jest": "^29.5.11",
|
||||||
|
"@types/minimist": "^1.2.5",
|
||||||
"@types/node": "^20.11.4",
|
"@types/node": "^20.11.4",
|
||||||
"@types/tar": "^6.1.10",
|
"@types/tar": "^6.1.10",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.12.0",
|
"@typescript-eslint/eslint-plugin": "^6.12.0",
|
||||||
|
|||||||
+39
-13
@@ -29,6 +29,27 @@ export interface FileMetadata {
|
|||||||
sha256: string
|
sha256: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: rename this function, it is not state-preserving, so it shouldn't just be called "get'"
|
||||||
|
export function getConsolidatedDirectory(filePathSpec: string): {
|
||||||
|
consolidatedPath: string
|
||||||
|
needToCleanUpDir: boolean
|
||||||
|
} {
|
||||||
|
const paths: string[] = filePathSpec.split(' ') // TODO: handle files with spaces
|
||||||
|
// TODO: do check on paths to make sure they're valid and not reaching outside the space
|
||||||
|
let consolidatedPath = ''
|
||||||
|
let needToCleanUpDir = false
|
||||||
|
if (paths.length === 1 && isDirectory(paths[0])) {
|
||||||
|
// If the path is a single directory, we can skip the bundling step
|
||||||
|
consolidatedPath = paths[0]
|
||||||
|
} else {
|
||||||
|
// Otherwise, we need to bundle the files & folders into a temporary directory
|
||||||
|
consolidatedPath = bundleFilesintoDirectory(paths)
|
||||||
|
needToCleanUpDir = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return { consolidatedPath, needToCleanUpDir }
|
||||||
|
}
|
||||||
|
|
||||||
// Creates both a tar.gz and zip archive of the given directory and returns the paths to both archives (stored in the provided target directory)
|
// Creates both a tar.gz and zip archive of the given directory and returns the paths to both archives (stored in the provided target directory)
|
||||||
// as well as the size/sha256 hash of each file.
|
// as well as the size/sha256 hash of each file.
|
||||||
export async function createArchives(
|
export async function createArchives(
|
||||||
@@ -55,7 +76,7 @@ export async function createArchives(
|
|||||||
})
|
})
|
||||||
|
|
||||||
archive.pipe(output)
|
archive.pipe(output)
|
||||||
archive.directory(distPath, false)
|
archive.directory(distPath, false) // TODO: make sure this doesn't include dirs that start with ., same with below
|
||||||
archive.finalize()
|
archive.finalize()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -91,25 +112,30 @@ export function isDirectory(dirPath: string): boolean {
|
|||||||
return fs.existsSync(dirPath) && fs.lstatSync(dirPath).isDirectory()
|
return fs.existsSync(dirPath) && fs.lstatSync(dirPath).isDirectory()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isActionRepo(stagingDir: string): boolean {
|
||||||
|
return (
|
||||||
|
fs.existsSync(path.join(stagingDir, 'action.yml')) ||
|
||||||
|
fs.existsSync(path.join(stagingDir, 'action.yaml'))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function readFileContents(filePath: string): Buffer {
|
export function readFileContents(filePath: string): Buffer {
|
||||||
return fs.readFileSync(filePath)
|
return fs.readFileSync(filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function bundleFilesintoDirectory(
|
function bundleFilesintoDirectory(filePaths: string[]): string {
|
||||||
files: string[],
|
const targetDir: string = createTempDir()
|
||||||
targetDir: string = createTempDir()
|
for (const filePath of filePaths) {
|
||||||
): string {
|
if (!fs.existsSync(filePath)) {
|
||||||
for (const file of files) {
|
throw new Error(`filePath ${filePath} does not exist`)
|
||||||
if (!fs.existsSync(file)) {
|
|
||||||
throw new Error(`File ${file} does not exist`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDirectory(file)) {
|
if (isDirectory(filePath)) {
|
||||||
const targetFolder = path.join(targetDir, path.basename(file))
|
const targetFolder = path.join(targetDir, path.basename(filePath)) // TODO: basename is probably not what we actually want here. Or is it? Maybe conflicts between dir1/dir2 and dir1/dir3/dir2 are just user error or ??
|
||||||
fsExtra.copySync(file, targetFolder)
|
fsExtra.copySync(filePath, targetFolder) // TODO: ignore files preceded by .
|
||||||
} else {
|
} else {
|
||||||
const targetFile = path.join(targetDir, path.basename(file))
|
const targetFile = path.join(targetDir, path.basename(filePath))
|
||||||
fs.copyFileSync(file, targetFile)
|
fs.copyFileSync(filePath, targetFile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -2,6 +2,8 @@
|
|||||||
* The entrypoint for the action.
|
* The entrypoint for the action.
|
||||||
*/
|
*/
|
||||||
import { run } from './main'
|
import { run } from './main'
|
||||||
|
import minimist from 'minimist'
|
||||||
|
|
||||||
|
const path = minimist(process.argv.slice(2)).path || '.'
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
run()
|
run(path)
|
||||||
|
|||||||
+34
-22
@@ -4,62 +4,61 @@ import * as fsHelper from './fs-helper'
|
|||||||
import * as ociContainer from './oci-container'
|
import * as ociContainer from './oci-container'
|
||||||
import * as ghcr from './ghcr-client'
|
import * as ghcr from './ghcr-client'
|
||||||
import semver from 'semver'
|
import semver from 'semver'
|
||||||
|
import crypto from 'crypto'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The main function for the action.
|
* The main function for the action.
|
||||||
* @returns {Promise<void>} Resolves when the action is complete.
|
* @returns {Promise<void>} Resolves when the action is complete.
|
||||||
*/
|
*/
|
||||||
export async function run(): Promise<void> {
|
export async function run(pathInput: string): Promise<void> {
|
||||||
const tmpDirs: string[] = []
|
const tmpDirs: string[] = []
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Parse and validate Actions execution context, including the repository name, release name and event type
|
// Parse and validate Actions execution context, including the repository name, release name and event type
|
||||||
const repository: string = process.env.GITHUB_REPOSITORY || ''
|
const repository: string = process.env.GITHUB_REPOSITORY || ''
|
||||||
|
|
||||||
if (repository === '') {
|
if (repository === '') {
|
||||||
core.setFailed(`Could not find Repository.`)
|
core.setFailed(`Could not find Repository.`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (github.context.eventName !== 'release') {
|
if (github.context.eventName !== 'release') {
|
||||||
core.setFailed('Please ensure you have the workflow trigger as release.')
|
core.setFailed('Please ensure you have the workflow trigger as release.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const releaseId: string = github.context.payload.release.id
|
const releaseId: string = github.context.payload.release.id
|
||||||
const releaseTag: string = github.context.payload.release.tag_name
|
const releaseTag: string = github.context.payload.release.tag_name
|
||||||
|
|
||||||
// Strip any leading 'v' from the tag in case the release format is e.g. 'v1.0.0' as recommended by GitHub docs
|
// Strip any leading 'v' from the tag in case the release format is e.g. 'v1.0.0' as recommended by GitHub docs
|
||||||
// https://docs.github.com/en/actions/creating-actions/releasing-and-maintaining-actions
|
// https://docs.github.com/en/actions/creating-actions/releasing-and-maintaining-actions
|
||||||
const targetVersion = semver.parse(releaseTag.replace(/^v/, ''))
|
const targetVersion = semver.parse(releaseTag.replace(/^v/, ''))
|
||||||
if (!targetVersion) {
|
if (!targetVersion) {
|
||||||
// TODO: We may want to limit semvers to only x.x.x, without the pre-release tags, but for now we'll allow them.
|
|
||||||
core.setFailed(
|
core.setFailed(
|
||||||
`${releaseTag} is not a valid semantic version, and so cannot be uploaded as an Immutable Action.`
|
`${releaseTag} is not a valid semantic version, and so cannot be uploaded as an Immutable Action.`
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gather & validate user inputs
|
const token: string = process.env.TOKEN!
|
||||||
const token: string = core.getInput('token')
|
|
||||||
const registryURL: URL = new URL(core.getInput('registry')) // TODO: Should this be dynamic? Maybe an API endpoint to grab the registry for GHES/proxima purposes.
|
|
||||||
|
|
||||||
// Paths to be included in the OCI image
|
const { consolidatedPath, needToCleanUpDir } =
|
||||||
const paths: string[] = core.getInput('path').split(' ')
|
fsHelper.getConsolidatedDirectory(pathInput)
|
||||||
let path = ''
|
if (needToCleanUpDir) {
|
||||||
|
tmpDirs.push(consolidatedPath)
|
||||||
|
}
|
||||||
|
|
||||||
if (paths.length === 1 && fsHelper.isDirectory(paths[0])) {
|
if (!fsHelper.isActionRepo(consolidatedPath)) {
|
||||||
// If the path is a single directory, we can skip the bundling step
|
core.setFailed(
|
||||||
path = paths[0]
|
'action.y(a)ml not found. Action packages can be created only for action repositories.'
|
||||||
} else {
|
)
|
||||||
// Otherwise, we need to bundle the files & folders into a temporary directory
|
return
|
||||||
const bundleDir = fsHelper.createTempDir()
|
|
||||||
tmpDirs.push(bundleDir)
|
|
||||||
path = fsHelper.bundleFilesintoDirectory(paths, bundleDir)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a temporary directory to store the archives
|
// Create a temporary directory to store the archives
|
||||||
const archiveDir = fsHelper.createTempDir()
|
const archiveDir = fsHelper.createTempDir()
|
||||||
tmpDirs.push(archiveDir)
|
tmpDirs.push(archiveDir)
|
||||||
|
|
||||||
const archives = await fsHelper.createArchives(path, archiveDir)
|
const archives = await fsHelper.createArchives(consolidatedPath, archiveDir)
|
||||||
|
|
||||||
const manifest = ociContainer.createActionPackageManifest(
|
const manifest = ociContainer.createActionPackageManifest(
|
||||||
archives.tarFile,
|
archives.tarFile,
|
||||||
@@ -69,6 +68,22 @@ export async function run(): Promise<void> {
|
|||||||
new Date()
|
new Date()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Generate SHA-256 hash of the manifest
|
||||||
|
const manifestSHA = crypto.createHash('sha256')
|
||||||
|
const manifestHash = manifestSHA
|
||||||
|
.update(JSON.stringify(manifest))
|
||||||
|
.digest('hex')
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${process.env.GITHUB_API_URL}/packages/container-registry-url`
|
||||||
|
)
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch status page: ${response.statusText}`)
|
||||||
|
}
|
||||||
|
const data = await response.json()
|
||||||
|
const registryURL: URL = new URL(data.url)
|
||||||
|
console.log(`Container registry URL: ${registryURL}`)
|
||||||
|
|
||||||
const packageURL = await ghcr.publishOCIArtifact(
|
const packageURL = await ghcr.publishOCIArtifact(
|
||||||
token,
|
token,
|
||||||
registryURL,
|
registryURL,
|
||||||
@@ -82,11 +97,8 @@ export async function run(): Promise<void> {
|
|||||||
)
|
)
|
||||||
|
|
||||||
core.setOutput('package-url', packageURL.toString())
|
core.setOutput('package-url', packageURL.toString())
|
||||||
|
|
||||||
// TODO: We might need to do some attestation stuff here, but unsure how to integrate it yet.
|
|
||||||
// We might need to return the manifest JSON from the Action and link it to another action,
|
|
||||||
// or we might be able to make an API call here. It's unclear at this point.
|
|
||||||
core.setOutput('package-manifest', JSON.stringify(manifest))
|
core.setOutput('package-manifest', JSON.stringify(manifest))
|
||||||
|
core.setOutput('package-manifest-sha', `sha256:${manifestHash}`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Fail the workflow run if an error occurs
|
// Fail the workflow run if an error occurs
|
||||||
if (error instanceof Error) core.setFailed(error.message)
|
if (error instanceof Error) core.setFailed(error.message)
|
||||||
|
|||||||
Reference in New Issue
Block a user