├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ ├── .vib │ ├── github-event.json │ ├── runtime-parameters.yaml │ └── vib-pipeline.json │ ├── release.yaml │ ├── smoke-test.yaml │ └── validate.yaml ├── .gitignore ├── .npmrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md ├── __tests__ ├── fixtures │ └── fixtures.ts ├── mother │ ├── execution-graph-report.ts │ ├── execution-graph.ts │ ├── pipeline.ts │ ├── target-platform.ts │ ├── task-report.ts │ └── task.ts ├── resources │ ├── .vib-other │ │ └── vib-pipeline-other.json │ ├── .vib │ │ ├── disfunctional-pipeline.json │ │ ├── pipeline-with-vib-envs.json │ │ ├── runtime-parameters-file.yaml │ │ ├── vib-pipeline-2.json │ │ ├── vib-pipeline-file.json │ │ ├── vib-pipeline.json │ │ └── vib-sha-archive.json │ ├── bundle-failed.zip │ ├── bundle-not-passed.zip │ ├── bundle.zip │ ├── github-event-path-branch.json │ ├── github-event-path.json │ └── github-event-scheduled.json └── src │ ├── action.it.test.ts │ ├── action.test.ts │ ├── client │ ├── clients.test.ts │ ├── csp.test.ts │ └── vib.test.ts │ ├── config.test.ts │ └── sanitize.test.ts ├── action.yml ├── dist ├── index.js ├── index.js.map ├── licenses.txt └── sourcemap-register.js ├── jest.config.js ├── openapitools.json ├── package-lock.json ├── package.json ├── src ├── action.ts ├── client │ ├── clients.ts │ ├── csp.ts │ └── vib.ts ├── config.ts ├── index.ts ├── sanitize.ts └── util.ts └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | node_modules/ 4 | jest.config.js 5 | src/client/vib/ 6 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["jest", "@typescript-eslint"], 3 | "extends": ["plugin:github/recommended"], 4 | "parser": "@typescript-eslint/parser", 5 | "parserOptions": { 6 | "ecmaVersion": 9, 7 | "sourceType": "module", 8 | "project": "./tsconfig.json" 9 | }, 10 | "rules": { 11 | "arrow-parens": ["error", "as-needed"], 12 | "camelcase": "off", 13 | "eslint-comments/no-use": "off", 14 | "i18n-text/no-en": "off", 15 | "import/no-namespace": "off", 16 | "indent": ["error", 2, { "SwitchCase": 1 }], 17 | "max-len": ["error", { 18 | "code": 140, 19 | "tabWidth": 2, 20 | "ignoreTemplateLiterals": true 21 | }], 22 | "no-extra-parens": "error", 23 | "no-shadow": "off", 24 | "no-unused-vars": "off", 25 | "prettier/prettier": "off", 26 | "semi": "off", 27 | "sort-imports": "off", 28 | "@typescript-eslint/no-unused-vars": "error", 29 | "@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}], 30 | "@typescript-eslint/no-require-imports": "error", 31 | "@typescript-eslint/array-type": "error", 32 | "@typescript-eslint/await-thenable": "error", 33 | "@typescript-eslint/ban-ts-comment": "error", 34 | "@typescript-eslint/consistent-type-assertions": "error", 35 | "@typescript-eslint/explicit-function-return-type": ["error", {"allowExpressions": true}], 36 | "@typescript-eslint/func-call-spacing": ["error", "never"], 37 | "@typescript-eslint/no-array-constructor": "error", 38 | "@typescript-eslint/no-empty-interface": "error", 39 | "@typescript-eslint/no-explicit-any": "error", 40 | "@typescript-eslint/no-extraneous-class": "error", 41 | "@typescript-eslint/no-for-in-array": "error", 42 | "@typescript-eslint/no-inferrable-types": "error", 43 | "@typescript-eslint/no-misused-new": "error", 44 | "@typescript-eslint/no-namespace": "error", 45 | "@typescript-eslint/no-non-null-assertion": "warn", 46 | "@typescript-eslint/no-shadow": "error", 47 | "@typescript-eslint/no-unnecessary-qualifier": "error", 48 | "@typescript-eslint/no-unnecessary-type-assertion": "error", 49 | "@typescript-eslint/no-useless-constructor": "error", 50 | "@typescript-eslint/no-var-requires": "error", 51 | "@typescript-eslint/prefer-for-of": "warn", 52 | "@typescript-eslint/prefer-function-type": "warn", 53 | "@typescript-eslint/prefer-includes": "error", 54 | "@typescript-eslint/prefer-string-starts-ends-with": "error", 55 | "@typescript-eslint/promise-function-async": "error", 56 | "@typescript-eslint/require-array-sort-compare": "error", 57 | "@typescript-eslint/restrict-plus-operands": "error", 58 | "@typescript-eslint/semi": ["error", "never"], 59 | "@typescript-eslint/type-annotation-spacing": "error", 60 | "@typescript-eslint/unbound-method": "error" 61 | }, 62 | "env": { 63 | "node": true, 64 | "es6": true, 65 | "jest/globals": true 66 | } 67 | } -------------------------------------------------------------------------------- /.github/workflows/.vib/github-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "pull_request": { 3 | "head": { 4 | "label": "bitnami:wordpress", 5 | "ref": "32282c823a92b2520e9ed8599822730c372f01be", 6 | "sha": "32282c823a92b2520e9ed8599822730c372f01be", 7 | "repo": { 8 | "id": 52300938, 9 | "node_id": "MDEwOlJlcG9zaXRvcnk1MjMwMDkzOA==", 10 | "name": "charts", 11 | "full_name": "bitnami/charts", 12 | "private": false, 13 | "owner": { 14 | "login": "bitnami", 15 | "id": 5446553, 16 | "node_id": "MDEyOk9yZ2FuaXphdGlvbjU0NDY1NTM=", 17 | "avatar_url": "https://avatars.githubusercontent.com/u/5446553?v=4", 18 | "gravatar_id": "", 19 | "url": "https://api.github.com/users/bitnami", 20 | "html_url": "https://github.com/bitnami", 21 | "followers_url": "https://api.github.com/users/bitnami/followers", 22 | "following_url": "https://api.github.com/users/bitnami/following{/other_user}", 23 | "gists_url": "https://api.github.com/users/bitnami/gists{/gist_id}", 24 | "starred_url": "https://api.github.com/users/bitnami/starred{/owner}{/repo}", 25 | "subscriptions_url": "https://api.github.com/users/bitnami/subscriptions", 26 | "organizations_url": "https://api.github.com/users/bitnami/orgs", 27 | "repos_url": "https://api.github.com/users/bitnami/repos", 28 | "events_url": "https://api.github.com/users/bitnami/events{/privacy}", 29 | "received_events_url": "https://api.github.com/users/bitnami/received_events", 30 | "type": "Organization", 31 | "site_admin": false 32 | }, 33 | "html_url": "https://github.com/bitnami/charts", 34 | "description": "Bitnami Helm Charts", 35 | "fork": false, 36 | "url": "https://api.github.com/repos/bitnami/charts", 37 | "forks_url": "https://api.github.com/repos/bitnami/charts/forks", 38 | "keys_url": "https://api.github.com/repos/bitnami/charts/keys{/key_id}", 39 | "collaborators_url": "https://api.github.com/repos/bitnami/charts/collaborators{/collaborator}", 40 | "teams_url": "https://api.github.com/repos/bitnami/charts/teams", 41 | "hooks_url": "https://api.github.com/repos/bitnami/charts/hooks", 42 | "issue_events_url": "https://api.github.com/repos/bitnami/charts/issues/events{/number}", 43 | "events_url": "https://api.github.com/repos/bitnami/charts/events", 44 | "assignees_url": "https://api.github.com/repos/bitnami/charts/assignees{/user}", 45 | "branches_url": "https://api.github.com/repos/bitnami/charts/branches{/branch}", 46 | "tags_url": "https://api.github.com/repos/bitnami/charts/tags", 47 | "blobs_url": "https://api.github.com/repos/bitnami/charts/git/blobs{/sha}", 48 | "git_tags_url": "https://api.github.com/repos/bitnami/charts/git/tags{/sha}", 49 | "git_refs_url": "https://api.github.com/repos/bitnami/charts/git/refs{/sha}", 50 | "trees_url": "https://api.github.com/repos/bitnami/charts/git/trees{/sha}", 51 | "statuses_url": "https://api.github.com/repos/bitnami/charts/statuses/{sha}", 52 | "languages_url": "https://api.github.com/repos/bitnami/charts/languages", 53 | "stargazers_url": "https://api.github.com/repos/bitnami/charts/stargazers", 54 | "contributors_url": "https://api.github.com/repos/bitnami/charts/contributors", 55 | "subscribers_url": "https://api.github.com/repos/bitnami/charts/subscribers", 56 | "subscription_url": "https://api.github.com/repos/bitnami/charts/subscription", 57 | "commits_url": "https://api.github.com/repos/bitnami/charts/commits{/sha}", 58 | "git_commits_url": "https://api.github.com/repos/bitnami/charts/git/commits{/sha}", 59 | "comments_url": "https://api.github.com/repos/bitnami/charts/comments{/number}", 60 | "issue_comment_url": "https://api.github.com/repos/bitnami/charts/issues/comments{/number}", 61 | "contents_url": "https://api.github.com/repos/bitnami/charts/contents/{+path}", 62 | "compare_url": "https://api.github.com/repos/bitnami/charts/compare/{base}...{head}", 63 | "merges_url": "https://api.github.com/repos/bitnami/charts/merges", 64 | "archive_url": "https://api.github.com/repos/bitnami/charts/{archive_format}{/ref}", 65 | "downloads_url": "https://api.github.com/repos/bitnami/charts/downloads", 66 | "issues_url": "https://api.github.com/repos/bitnami/charts/issues{/number}", 67 | "pulls_url": "https://api.github.com/repos/bitnami/charts/pulls{/number}", 68 | "milestones_url": "https://api.github.com/repos/bitnami/charts/milestones{/number}", 69 | "notifications_url": "https://api.github.com/repos/bitnami/charts/notifications{?since,all,participating}", 70 | "labels_url": "https://api.github.com/repos/bitnami/charts/labels{/name}", 71 | "releases_url": "https://api.github.com/repos/bitnami/charts/releases{/id}", 72 | "deployments_url": "https://api.github.com/repos/bitnami/charts/deployments", 73 | "created_at": "2016-02-22T19:51:26Z", 74 | "updated_at": "2023-01-11T10:13:43Z", 75 | "pushed_at": "2023-01-11T11:15:43Z", 76 | "git_url": "git://github.com/bitnami/charts.git", 77 | "ssh_url": "git@github.com:bitnami/charts.git", 78 | "clone_url": "https://github.com/bitnami/charts.git", 79 | "svn_url": "https://github.com/bitnami/charts", 80 | "homepage": "https://bitnami.com", 81 | "size": 1027785, 82 | "stargazers_count": 6526, 83 | "watchers_count": 6526, 84 | "language": "Mustache", 85 | "has_issues": true, 86 | "has_projects": true, 87 | "has_downloads": true, 88 | "has_wiki": false, 89 | "has_pages": false, 90 | "has_discussions": false, 91 | "forks_count": 7458, 92 | "mirror_url": null, 93 | "archived": false, 94 | "disabled": false, 95 | "open_issues_count": 151, 96 | "license": { 97 | "key": "apache-2.0", 98 | "name": "Apache License 2.0", 99 | "spdx_id": "Apache-2.0", 100 | "url": "https://api.github.com/licenses/apache-2.0", 101 | "node_id": "MDc6TGljZW5zZTI=" 102 | }, 103 | "allow_forking": true, 104 | "is_template": false, 105 | "web_commit_signoff_required": true, 106 | "topics": [ 107 | "bitnami", 108 | "charts", 109 | "helm", 110 | "helm-charts", 111 | "kubernetes", 112 | "vmware" 113 | ], 114 | "visibility": "public", 115 | "forks": 7458, 116 | "open_issues": 151, 117 | "watchers": 6526, 118 | "default_branch": "main" 119 | } 120 | } 121 | } 122 | } -------------------------------------------------------------------------------- /.github/workflows/.vib/runtime-parameters.yaml: -------------------------------------------------------------------------------- 1 | wordpressUsername: test_user 2 | wordpressPassword: ComplicatedPassword123!4 3 | wordpressEmail: test_user_email@email.com 4 | wordpressFirstName: TestName 5 | wordpressLastName: TestLastName 6 | wordpressBlogName: Test_Users's Blog! 7 | smtpHost: mail.server.com 8 | smtpPort: 120 9 | smtpUser: test_mail_user 10 | smtpPassword: test_mail_password 11 | mariadb: 12 | auth: 13 | database: test_wordpress_database 14 | username: test_wordpress_username 15 | password: test_wordpress_password 16 | containerSecurityContext: 17 | enabled: true 18 | runAsUser: 1002 19 | runAsNonRoot: true 20 | wordpressTablePrefix: wordpress_ 21 | -------------------------------------------------------------------------------- /.github/workflows/.vib/vib-pipeline.json: -------------------------------------------------------------------------------- 1 | { 2 | "phases": { 3 | "package": { 4 | "context": { 5 | "resources": { 6 | "url": "{SHA_ARCHIVE}", 7 | "path": "/bitnami/wordpress" 8 | } 9 | }, 10 | "actions": [ 11 | { 12 | "action_id": "helm-package" 13 | } 14 | ] 15 | }, 16 | "verify": { 17 | "context": { 18 | "resources": { 19 | "url": "{SHA_ARCHIVE}", 20 | "path": "/bitnami/wordpress" 21 | }, 22 | "target_platform": { 23 | "target_platform_id": "{VIB_ENV_TARGET_PLATFORM}" 24 | } 25 | }, 26 | "actions": [ 27 | { 28 | "action_id": "trivy", 29 | "params": { 30 | "threshold": "IGNORE_ALL" 31 | } 32 | }, 33 | { 34 | "action_id": "cypress", 35 | "params": { 36 | "resources": { 37 | "path": "/.vib/wordpress/cypress" 38 | }, 39 | "endpoint": "lb-wordpress-https", 40 | "app_protocol": "HTTPS", 41 | "env": { 42 | "username": "test_user", 43 | "password": "ComplicatedPassword123!4" 44 | } 45 | } 46 | } 47 | ] 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release Action 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | version: 6 | description: 'Release version (i.e. v1.2.3)' 7 | required: true 8 | type: string 9 | jobs: 10 | release: 11 | name: Release Action 12 | runs-on: ubuntu-latest 13 | if: ${{ github.ref == 'refs/heads/main' }} 14 | steps: 15 | - uses: actions/checkout@v3 16 | name: Checkout Repository 17 | with: 18 | token: ${{ secrets.VIB_ACTION_TOKEN }} 19 | 20 | - name: Set Node.js 20 21 | uses: actions/setup-node@v2 22 | with: 23 | node-version: 20.x 24 | 25 | - name: Git config 26 | run: | 27 | git config user.name github-actions 28 | git config user.email github-actions@github.com 29 | 30 | - name: Release version 31 | run: npm version --allow-same-version=true ${{ inputs.version }} -m "[AUTOMATED] Release version %s" 32 | 33 | - name: Roll major tag 34 | run: | 35 | VERSION="${{ inputs.version }}" 36 | VERSION_MAJOR="${VERSION%%\.*}" 37 | git tag -f $VERSION_MAJOR ${{ inputs.version }} 38 | 39 | - name: Push rolling major tag 40 | run: git push --tags --force -------------------------------------------------------------------------------- /.github/workflows/smoke-test.yaml: -------------------------------------------------------------------------------- 1 | name: Action Smoke Tests 2 | on: workflow_dispatch 3 | jobs: 4 | run-action: 5 | name: Run GH Action 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v3 9 | name: Checkout Repository 10 | - uses: ./ 11 | name: Run GH Action on ${{ github.ref }} 12 | env: 13 | CSP_API_TOKEN: ${{ secrets.CSP_API_TOKEN }} 14 | GITHUB_EVENT_PATH_OVERRIDE: .github/workflows/.vib/github-event.json 15 | VIB_ENV_TARGET_PLATFORM: 91d398a2-25c4-4cda-8732-75a3cfc179a1 16 | with: 17 | config: .github/workflows/.vib 18 | runtime-parameters-file: runtime-parameters.yaml -------------------------------------------------------------------------------- /.github/workflows/validate.yaml: -------------------------------------------------------------------------------- 1 | name: Validate 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | types: 10 | - assigned 11 | - opened 12 | - synchronize 13 | - reopened 14 | env: 15 | CSP_API_URL: https://console.tanzu.broadcom.com 16 | CSP_API_TOKEN: ${{ secrets.CSP_API_TOKEN }} 17 | VIB_PUBLIC_URL: https://cp.bromelia.vmware.com 18 | jobs: 19 | build: 20 | name: Build and Test 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v2 25 | 26 | - name: Set Node.js 20 27 | uses: actions/setup-node@v2 28 | with: 29 | node-version: 20.x 30 | 31 | - name: Install dependencies 32 | run: npm ci 33 | 34 | - name: Compile 35 | run: npm run build 36 | 37 | - name: Lint 38 | run: npm run lint 39 | 40 | - name: Package 41 | run: npm run package 42 | 43 | - name: Compare the expected and actual dist/ directories 44 | run: | 45 | if [ "$(git diff --ignore-space-at-eol dist/ | wc -l)" -gt "0" ]; then 46 | echo "Detected $(git diff --ignore-space-at-eol dist/ | wc -l) uncommitted changes after build." 47 | exit 1 48 | fi 49 | id: diff 50 | 51 | - name: Functional Tests 52 | run: npm test 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | 3 | # Dependency directory 4 | node_modules 5 | 6 | # Rest pulled from https://github.com/github/gitignore/blob/master/Node.gitignore 7 | # Logs 8 | logs 9 | __tests__/logs 10 | reports 11 | outputs 12 | *.log 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | lerna-debug.log* 17 | 18 | # Diagnostic reports (https://nodejs.org/api/report.html) 19 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 20 | 21 | # Runtime data 22 | pids 23 | *.pid 24 | *.seed 25 | *.pid.lock 26 | 27 | # Directory for instrumented libs generated by jscoverage/JSCover 28 | lib-cov 29 | 30 | # Coverage directory used by tools like istanbul 31 | coverage 32 | *.lcov 33 | 34 | # nyc test coverage 35 | .nyc_output 36 | 37 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 38 | .grunt 39 | 40 | # Bower dependency directory (https://bower.io/) 41 | bower_components 42 | 43 | # node-waf configuration 44 | .lock-wscript 45 | 46 | # Compiled binary addons (https://nodejs.org/api/addons.html) 47 | build/Release 48 | 49 | # Dependency directories 50 | jspm_packages/ 51 | 52 | # TypeScript v1 declaration files 53 | typings/ 54 | 55 | # TypeScript cache 56 | *.tsbuildinfo 57 | 58 | # Optional npm cache directory 59 | .npm 60 | 61 | # Optional eslint cache 62 | .eslintcache 63 | 64 | # Optional REPL history 65 | .node_repl_history 66 | 67 | # Output of 'npm pack' 68 | *.tgz 69 | 70 | # Yarn Integrity file 71 | .yarn-integrity 72 | 73 | # dotenv environment variables file 74 | .env 75 | .env.test 76 | 77 | # parcel-bundler cache (https://parceljs.org/) 78 | .cache 79 | 80 | # next.js build output 81 | .next 82 | 83 | # nuxt.js build output 84 | .nuxt 85 | 86 | # vuepress build output 87 | .vuepress/dist 88 | 89 | # Serverless directories 90 | .serverless/ 91 | 92 | # FuseBox cache 93 | .fusebox/ 94 | 95 | # DynamoDB Local files 96 | .dynamodb/ 97 | 98 | # OS metadata 99 | .DS_Store 100 | Thumbs.db 101 | 102 | # Ignore built ts files 103 | __tests__/runner/* 104 | lib/**/* 105 | 106 | # Autogenerated clients 107 | src/client/vib 108 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in vmware-image-builder-action project and our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at oss-coc@@vmware.com. 63 | All complaints will be reviewed and investigated promptly and fairly. 64 | 65 | All community leaders are obligated to respect the privacy and security of the 66 | reporter of any incident. 67 | 68 | ## Enforcement Guidelines 69 | 70 | Community leaders will follow these Community Impact Guidelines in determining 71 | the consequences for any action they deem in violation of this Code of Conduct: 72 | 73 | ### 1. Correction 74 | 75 | **Community Impact**: Use of inappropriate language or other behavior deemed 76 | unprofessional or unwelcome in the community. 77 | 78 | **Consequence**: A private, written warning from community leaders, providing 79 | clarity around the nature of the violation and an explanation of why the 80 | behavior was inappropriate. A public apology may be requested. 81 | 82 | ### 2. Warning 83 | 84 | **Community Impact**: A violation through a single incident or series 85 | of actions. 86 | 87 | **Consequence**: A warning with consequences for continued behavior. No 88 | interaction with the people involved, including unsolicited interaction with 89 | those enforcing the Code of Conduct, for a specified period of time. This 90 | includes avoiding interactions in community spaces as well as external channels 91 | like social media. Violating these terms may lead to a temporary or 92 | permanent ban. 93 | 94 | ### 3. Temporary Ban 95 | 96 | **Community Impact**: A serious violation of community standards, including 97 | sustained inappropriate behavior. 98 | 99 | **Consequence**: A temporary ban from any sort of interaction or public 100 | communication with the community for a specified period of time. No public or 101 | private interaction with the people involved, including unsolicited interaction 102 | with those enforcing the Code of Conduct, is allowed during this period. 103 | Violating these terms may lead to a permanent ban. 104 | 105 | ### 4. Permanent Ban 106 | 107 | **Community Impact**: Demonstrating a pattern of violation of community 108 | standards, including sustained inappropriate behavior, harassment of an 109 | individual, or aggression toward or disparagement of classes of individuals. 110 | 111 | **Consequence**: A permanent ban from any sort of public interaction within 112 | the community. 113 | 114 | ## Attribution 115 | 116 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 117 | version 2.0, available at 118 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 119 | 120 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 121 | enforcement ladder](https://github.com/mozilla/diversity). 122 | 123 | [homepage]: https://www.contributor-covenant.org 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | https://www.contributor-covenant.org/faq. Translations are available at 127 | https://www.contributor-covenant.org/translations. 128 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to vmware-image-builder-action 2 | 3 | The vmware-image-builder-action project team welcomes contributions from the community. Before you start working with vmware-image-builder-action, please 4 | read our [Developer Certificate of Origin](https://cla.vmware.com/dco). All contributions to this repository must be 5 | signed as described on that page. Your signature certifies that you wrote the patch or have the right to pass it on 6 | as an open-source patch. 7 | 8 | ## Contribution Flow 9 | 10 | This is a rough outline of what a contributor's workflow looks like: 11 | 12 | - Create a topic branch from where you want to base your work 13 | - Make commits of logical units 14 | - Make sure your commit messages are in the proper format (see below) 15 | - Push your changes to a topic branch in your fork of the repository 16 | - Submit a pull request 17 | 18 | Example: 19 | 20 | ``` shell 21 | git remote add upstream https://github.com/vmware-labs/vmware-image-builder-action.git 22 | git checkout -b my-new-feature main 23 | git commit -a 24 | git push origin my-new-feature 25 | ``` 26 | 27 | ### Staying In Sync With Upstream 28 | 29 | When your branch gets out of sync with the vmware-labs/main branch, use the following to update: 30 | 31 | ``` shell 32 | git checkout my-new-feature 33 | git fetch -a 34 | git pull --rebase upstream main 35 | git push --force-with-lease origin my-new-feature 36 | ``` 37 | 38 | ### Updating pull requests 39 | 40 | If your PR fails to pass CI or needs changes based on code review, you'll most likely want to squash these changes into 41 | existing commits. 42 | 43 | If your pull request contains a single commit or your changes are related to the most recent commit, you can simply 44 | amend the commit. 45 | 46 | ``` shell 47 | git add . 48 | git commit --amend 49 | git push --force-with-lease origin my-new-feature 50 | ``` 51 | 52 | If you need to squash changes into an earlier commit, you can use: 53 | 54 | ``` shell 55 | git add . 56 | git commit --fixup 57 | git rebase -i --autosquash main 58 | git push --force-with-lease origin my-new-feature 59 | ``` 60 | 61 | Be sure to add a comment to the PR indicating your new changes are ready to review, as GitHub does not generate a 62 | notification when you git push. 63 | 64 | ### Code Style 65 | 66 | ### Formatting Commit Messages 67 | 68 | We follow the conventions on [How to Write a Git Commit Message](http://chris.beams.io/posts/git-commit/). 69 | 70 | Be sure to include any related GitHub issue references in the commit message. See 71 | [GFM syntax](https://guides.github.com/features/mastering-markdown/#GitHub-flavored-markdown) for referencing issues 72 | and commits. 73 | 74 | ### Formatting Code 75 | 76 | We do use ESLint as a tool to ensure a consistent format. A format check step runs as part of our continuous integration and pull requests get failed checks if they don't adhere to the conventions. To make sure your contribution is properly formatted you can run the following: 77 | 78 | ``` shell 79 | npm run lint 80 | ``` 81 | 82 | The `format` script can also be used to make ESLint automatically apply the format guidelines: 83 | 84 | ``` shell 85 | npm run format 86 | ``` 87 | 88 | ### Static Analysis 89 | 90 | As part of our continuous integration workflow we do pass ESlint to all pull requests. To make sure that your contribution passes all the static checks you can use: 91 | 92 | ``` shell 93 | npm run lint 94 | ``` 95 | 96 | ### Tests 97 | 98 | We value highly-tested software. All pull requests should come accompanied by corresponding unit tests. Integration tests are also welcomed. To make sure that your contribution isn't causing any regression we run the test suite as part of our continuous integration process. You should also make sure that all the tests pass before sending your pull request: 99 | 100 | ``` shell 101 | npm run test 102 | ``` 103 | 104 | ## Release Process 105 | 106 | All stable code is hosted at the `main` branch. Releases are done on demand through the _Release Action_ GitHub workflow. In order to release the current `HEAD`, you will need to trigger this workflow passing the version being released (i.e. `v3.0.2`). 107 | 108 | Once triggered, the workflow will put the specified version on the `package.json` and `package-lock.json`, generate a release commit and tag, and roll the corresponding major. Then, all of that will be pushed back to GitHub. This mechanism lets users: 109 | 110 | * Stay synced with the main branch (`@main`) 111 | * Select a specific major train (`@v1`) 112 | * Pin a specific release (`@v1.2.3`) 113 | 114 | ## Promotion Process 115 | 116 | Upon any major release and sometimes with minor releases that might be needed by customers we will be promoting our releases to the GitHub Marketplace. Unfortunately, GitHub is not providing any automation for publishing GitHub Actions into their marketplace so this will essentially be a manual process until automation support is provided by their platform. 117 | 118 | ## Reporting Bugs and Creating Issues 119 | 120 | When opening a new issue, try to roughly follow the commit message format conventions above. 121 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Redistribution and use in source and binary forms, with or without 2 | modification, are permitted provided that the following conditions are 3 | met: 4 | 5 | 1. Redistributions of source code must retain the above copyright 6 | notice, this list of conditions and the following disclaimer. 7 | 8 | 2. Redistributions in binary form must reproduce the above 9 | copyright notice, this list of conditions and the following 10 | disclaimer in the documentation and/or other materials provided 11 | with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 14 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 15 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 16 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 17 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 18 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 19 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 20 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 21 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2022 VMware, Inc. 2 | 3 | This product is licensed to you under the BSD 2 clause (the "License"). You may not use this product except in compliance with the License. 4 | 5 | This product may include a number of subcomponents with separate copyright notices and license terms. Your use of these subcomponents is subject to the terms and conditions of the subcomponent's license, as noted in the LICENSE file. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VMware Image Builder 2 | 3 | ## Overview 4 | 5 | This GitHub Action allows to interact with the VMware Image Builder (VIB) service from VMware. VIB is a SaaS product that can be used by Independent Software Vendors (ISV) to Package, Verify and Publish their software products. These products can be packaged in different formats like for example [Carvel Packages](https://carvel.dev), [Helm Charts](https://helm.sh) or [Open Virtual Appliances](https://docs.vmware.com/en/VMware-vSphere/7.0/com.vmware.vsphere.vm_admin.doc/GUID-AE61948B-C2EE-436E-BAFB-3C7209088552.html) (OVA). Container images that use a Dockerfile are also supported for being packaged and so are any projects built on [buildpacks](https://buildpacks.io). 6 | 7 | VIB supports verification in multiple Kubernetes distributions and flavours, like for example TKG, GKE, AKS, EKS, IKS and OpenShift, and also does support vSphere for OVAs. In addition to functional verification, VIB does offer compliance verification with support for static analysis and some popular tools like Trivy or Grype for vulnerability scanning. For publishing software, OCI registries are supported. 8 | 9 | [VMware Image Builder Helps Verify Customized, Secure Software for Any Platform on Any Cloud](https://tanzu.vmware.com/content/blog/vmware-image-builder-verifies-customized-secure-software) is a good introductory article about how Carto, one of our partners is using VIB for verifying their Helm Chart from their own Supply Chain. 10 | 11 | ## Requirements 12 | 13 | Before using this GitHub Action you need to have a valid API Token. Valid tokens can be obtained by [signing up](https://console.tanzu.broadcom.com) to VMware Cloud Services and following these instructions. 14 | 15 | Once you have a valid api token you will need to set that **API token as a repository secret**. Your workflow then needs to make that secret available as an environment variable to the GitHub Action. 16 | 17 | ## Usage 18 | 19 | Once you have a valid token exposed as secret, ten using the GitHub Action is very simple. Here below you can find what would be a totally valid GitHub workflow that is using this action: 20 | 21 | ```yaml 22 | name: 'vib' 23 | on: 24 | pull_request 25 | env: 26 | CSP_API_URL: https://console.tanzu.broadcom.com 27 | CSP_API_TOKEN: ${{ secrets.CSP_API_TOKEN }} 28 | VIB_PUBLIC_URL: https://cp.bromelia.vmware.com 29 | jobs: 30 | validation: 31 | name: Validate 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@v2 35 | - uses: vmware-labs/vmware-image-builder-action@main 36 | ``` 37 | 38 | ### Action Input Parameters 39 | 40 | The above line is using the GitHub Action default input parameters. You can customize those parameters if you need to, and in fact, this will be pretty common when you have multiple pipelines that need to be sent to VIB: 41 | 42 | | Attribute | Description | Default value | 43 | | ------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | 44 | | backoff-intervals | This is the default backoff time (in milliseconds) used between each retry in case of failure reaching out to VIB. | [5000, 10000, 15000] | 45 | | config | This is the default folder where the action can find the configuration files for the different tasks that will be executed as part of the pipeline. | .vib | 46 | | http-timeout | This is the default number of milliseconds the GitHub Action waits for an HTTP timeout before failing. | 120000 | 47 | | max-pipeline-duration | This parameter specifies the time in seconds for a pipeline execution to be completed. | 5400 | 48 | | only-upload-on-failure | This parameter sets whether the GitHub Actions should upload artifacts for every task or only for those tasks that have failed. | true | 49 | | pipeline | This is the default JSON file that contains the VIB pipeline that will be executed. | vib-pipeline.json | 50 | | retry-count | This is the default number of retries to do in case of failure reaching out to VIB. | 3 | 51 | | runtime-parameters-file | This parameter specifies the location of the file with the runtime parameters in plain text. | runtime-parameters-file.yaml | 52 | | upload-artifacts | This parameter specifies whether the GitHub Action will publish logs and reports as GitHub artifacts. | true | 53 | | verification-mode | This parameter changes the default parallel verification mode to serial. | PARALLEL | 54 | 55 | With that in mind, you can customize your action as follows: 56 | 57 | ```yaml 58 | steps: 59 | - uses: actions/checkout@v2 60 | - uses: vmware-labs/vmware-image-builder-action@main 61 | with: 62 | config: redis-chart-tests 63 | pipeline: vib-platform-verify.json 64 | ``` 65 | 66 | ## Templating your pipelines via environment variables 67 | 68 | Pipelines can be templated via environment variables to allow further customization. Any environment variable that your workflow defines with the `VIB_ENV_` prefix will be substituted by the GitHub Action in the pipeline file before being sent to VIB. Furthermore, the GitHub Action will make this substitution independently of whether you are using the `VIB_ENV_` prefix in your pipeline or not. 69 | 70 | For example, if you had the following step: 71 | 72 | ```yaml 73 | steps: 74 | - uses: vmware-labs/vmware-image-builder-action@main 75 | env: 76 | VIB_ENV_PATH: /bitnami/redis 77 | ``` 78 | 79 | and part of your pipeline looks like: 80 | 81 | ```json 82 | { 83 | "phases": { 84 | "package": { 85 | "context": { 86 | "resources": { 87 | "path": "{PATH}" 88 | } 89 | } 90 | } 91 | } 92 | } 93 | ``` 94 | 95 | The GitHub Action will find the `{PATH}` template variable and will substitute it with the value from the `VIB_ENV_PATH` environment variable resulting in the following snipped being used when sending the pipeline to VIB: 96 | 97 | ```json 98 | { 99 | "phases": { 100 | "package": { 101 | "context": { 102 | "resources": { 103 | "path": "/bitnami/redis" 104 | } 105 | } 106 | } 107 | } 108 | } 109 | ``` 110 | 111 | `VIB_ENV` variable substitution can be a powerful mechanism to make your workloads more flexible and to reuse pipelines. 112 | 113 | ## Special variables 114 | 115 | There are a number of special variables that can be used as shortcuts. Here we will keep a list of those 116 | 117 | * `{SHA_ARCHIVE}`: Points to the HEAD of the change that has triggered the workflow, either from the main branch or a pull request. 118 | 119 | ## Enabling debug logging 120 | 121 | Sometimes the Github Action experiences problems, but the workflow logs do not provide enough information to figure out why it is not working as expected. In this case, it is possible to enable additional debug logging. 122 | 123 | To enable step debug logging, you must set the following secret in the repository that contains the workflow: `ACTIONS_STEP_DEBUG` to `true`. 124 | 125 | ## Contributing 126 | 127 | The vmware-image-builder-action project team welcomes contributions from the community. Before you start working with vmware-image-builder-action, please 128 | read our [Developer Certificate of Origin](https://cla.vmware.com/dco). All contributions to this repository must be 129 | signed as described on that page. Your signature certifies that you wrote the patch or have the right to pass it on 130 | as an open-source patch. For more detailed information, refer to [CONTRIBUTING.md](CONTRIBUTING.md). 131 | 132 | ## License 133 | 134 | VMware Image Builder Action 135 | Copyright 2022 VMware, Inc. 136 | 137 | The BSD-2 license (the "License") set forth below applies to all parts of the VMware Image Builder Examples project. You may not use this file except in compliance with the License. 138 | 139 | BSD-2 License 140 | 141 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 142 | 143 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 144 | 145 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 146 | 147 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 148 | -------------------------------------------------------------------------------- /__tests__/fixtures/fixtures.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import fs from "fs" 3 | import { Readable } from 'stream' 4 | 5 | export function runtimeParameters() { 6 | return fs.readFileSync(path.join(__dirname, '..', 'resources', '.vib', 'runtime-parameters-file.yaml')) 7 | .toString() 8 | .trim() 9 | } 10 | 11 | export function bundle(): Readable { 12 | return fs.createReadStream(path.join(__dirname, '..', 'resources', 'bundle.zip' )) 13 | } 14 | 15 | export function executionGraphNotPassed(): Readable { 16 | return fs.createReadStream(path.join(__dirname, '..', 'resources', 'bundle-not-passed.zip' )) 17 | } 18 | 19 | export function executionGraphFailed(): Readable { 20 | return fs.createReadStream(path.join(__dirname, '..', 'resources', 'bundle-failed.zip' )) 21 | } 22 | -------------------------------------------------------------------------------- /__tests__/mother/execution-graph-report.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionGraphReport } from '../../src/client/vib/api' 2 | 3 | export function report(passed = true): ExecutionGraphReport { 4 | return { 5 | passed: passed, 6 | actions: [ 7 | { 8 | task_id: '8b9ea8f0-06d7-4332-b353-afcbadfc89ea', 9 | action_id: 'trivy', 10 | passed: false, 11 | vulnerabilities: { 12 | minimal: 6, 13 | low: 5, 14 | medium: 4, 15 | high: 3, 16 | critical: 2, 17 | unknown: 1 18 | } 19 | }, 20 | { 21 | task_id: 'd426abec-4d9e-44d1-b540-0448197d5651', 22 | action_id: 'cypress', 23 | passed: true, 24 | tests: { 25 | passed: 3, 26 | skipped: 2, 27 | failed: 1 28 | } 29 | } 30 | ] 31 | } 32 | } -------------------------------------------------------------------------------- /__tests__/mother/execution-graph.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionGraph, Task, TaskStatus } from "../../src/client/vib/api" 2 | 3 | export function empty( 4 | execution_graph_id = '703f96d7-cf0f-4f6a-ba70-d6e9ee322aa1', 5 | status: TaskStatus = TaskStatus.Succeeded, 6 | tasks: Task[] = []): ExecutionGraph { 7 | return { 8 | created_at: '2022-11-11T16:23:59.427141Z', 9 | started_at: '2022-11-11T16:24:00.654397Z', 10 | execution_time: 1811, 11 | execution_graph_id, 12 | status, 13 | tasks 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /__tests__/mother/pipeline.ts: -------------------------------------------------------------------------------- 1 | import { Pipeline } from "../../src/client/vib/api" 2 | 3 | export function valid(): Pipeline { 4 | return { 5 | phases: { 6 | package: { 7 | actions: [ 8 | { 9 | action_id: 'helm-package', 10 | params: { 11 | resources: { 12 | url: 'https://github.com/bitnami/charts/tarball/d8a5f63aa65655f819bbd5d31f0be3c6c488e85c', 13 | path: 'bitnami/wordpress' 14 | } 15 | } 16 | } 17 | ] 18 | } 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /__tests__/mother/target-platform.ts: -------------------------------------------------------------------------------- 1 | import { TargetPlatform, TargetPlatformArchitecture, TargetPlatformKind, TargetPlatformProvider } from "../../src/client/vib/api" 2 | 3 | export function gke(): TargetPlatform { 4 | return { 5 | architecture: TargetPlatformArchitecture.Amd64, 6 | id: '91d398a2-25c4-4cda-8732-75a3cfc179a1', 7 | name: 'GKE Kubernetes v1.24.x', 8 | kind: TargetPlatformKind.Gke, 9 | default_version: '1.27', 10 | supported_versions: ["1.27"], 11 | provider: TargetPlatformProvider.Gcp 12 | } 13 | } -------------------------------------------------------------------------------- /__tests__/mother/task-report.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 2 | export function passed(): {[key: string]: any} { 3 | return { 4 | report: { 5 | passed: true, 6 | } 7 | } 8 | } 9 | 10 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 11 | export function failed(): {[key: string]: any} { 12 | return { 13 | report: { 14 | passed: false, 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /__tests__/mother/task.ts: -------------------------------------------------------------------------------- 1 | import { Phase, Task, TaskStatus } from '../../src/client/vib/api' 2 | 3 | export function cypress(task_id = 'd426abec-4d9e-44d1-b540-0448197d5651', status: TaskStatus = TaskStatus.Succeeded): Task { 4 | return { 5 | action_id: 'cypress', 6 | action_version: '0.113.0', 7 | status, 8 | execution_time: 113, 9 | started_at: '2023-01-19T14:16:08.364005Z', 10 | phase: Phase.Verify, 11 | task_id, 12 | previous_tasks: [], 13 | next_tasks: [], 14 | preconditions: [], 15 | params: { 16 | 'port': '80', 17 | 'host': '192.168.122.123', 18 | 'resources': { 19 | 'path': '/examples/wordpress/cypress', 20 | 'url': 'https://github.com/testproject/' 21 | }, 22 | 'app_protocol': 'HTTP', 23 | 'env': { 24 | 'password': 'test_password', 25 | 'username': 'test_user' 26 | } 27 | } 28 | } 29 | } 30 | 31 | export function deployment(task_id = '413e631d-0692-48de-ad4e-3962620b8f40', status: TaskStatus = TaskStatus.Succeeded, 32 | next_task?: string): Task { 33 | return { 34 | action_id: 'deployment', 35 | action_version: '0.1.0', 36 | status, 37 | execution_time: 123, 38 | started_at: '2023-01-19T14:16:08.364005Z', 39 | phase: Phase.Verify, 40 | task_id, 41 | previous_tasks: [], 42 | next_tasks: next_task ? [ next_task ] : [], 43 | preconditions: [], 44 | params: { 45 | 'application': { 46 | 'kind': 'HELM', 47 | 'details': { 48 | 'name': 'wordpress', 49 | 'repository': { 50 | 'url': 'oci://docker.io/bitnami/charts' 51 | }, 52 | 'version': '15.0.4' 53 | } 54 | } 55 | } 56 | } 57 | } 58 | 59 | export function helmPackage(task_id = '7447f3d8-f7e8-44b8-a516-3b17ddda24f3', status: TaskStatus = TaskStatus.Succeeded, 60 | next_task?: string): Task { 61 | return { 62 | action_id: 'helm-package', 63 | action_version: '0.113.0', 64 | status, 65 | execution_time: 19, 66 | started_at: '2023-01-19T14:16:08.364005Z', 67 | phase: Phase.Package, 68 | task_id, 69 | previous_tasks: [], 70 | next_tasks: next_task ? [ next_task ] : [], 71 | preconditions: [], 72 | params: { 73 | 'resources': { 74 | 'path': '/bitnami/wordpress', 75 | 'url': 'https://github.com/bitnami/charts/tarball/32282c823a92b2520e9ed8599822730c372f01be' 76 | } 77 | } 78 | } 79 | 80 | } 81 | 82 | export function trivy(task_id = '8b9ea8f0-06d7-4332-b353-afcbadfc89ea', status: TaskStatus = TaskStatus.Succeeded): Task { 83 | return { 84 | action_id: 'trivy', 85 | action_version: '0.113.0', 86 | status, 87 | execution_time: 29, 88 | started_at: '2023-01-19T14:16:08.226643Z', 89 | phase: Phase.Verify, 90 | task_id, 91 | previous_tasks: [], 92 | next_tasks: [], 93 | preconditions: [], 94 | params: { 95 | 'allowlist': [], 96 | 'threshold': 'IGNORE_ALL', 97 | 'application': { 98 | 'kind': 'HELM', 99 | 'details': { 100 | 'name': 'wordpress', 101 | 'repository': { 102 | 'url': 'oci://docker.io/bitnami/charts' 103 | }, 104 | 'version': '15.0.4' 105 | } 106 | } 107 | } 108 | } 109 | } -------------------------------------------------------------------------------- /__tests__/resources/.vib-other/vib-pipeline-other.json: -------------------------------------------------------------------------------- 1 | { 2 | "phases": { 3 | "package": { 4 | "context": { 5 | "resources": { 6 | "url": "https://github.com/bitnami/charts/tarball/d8a5f63aa65655f819bbd5d31f0be3c6c488e85c", 7 | "path": "/bitnami/wordpress" 8 | } 9 | }, 10 | "actions": [ 11 | { 12 | "action_id": "linter-packaging", 13 | "params": { 14 | "kind": "HELM" 15 | } 16 | } 17 | ] 18 | }, 19 | "verify": { 20 | "context": { 21 | "application": { 22 | "kind": "HELM", 23 | "details": { 24 | "name": "wordpress", 25 | "version": "12.1.24", 26 | "repository": { 27 | "url": "https://charts.bitnami.com/bitnami" 28 | } 29 | }, 30 | "values": "d29yZHByZXNzUGFzc3dvcmQ6IFMzOUJLV2pTa2gKbWFyaWFkYjoKICBhdXRoOgogICAgcGFzc3dvcmQ6IFZxbDVSR2RjbzQKICAgIHJvb3RQYXNzd29yZDogVUM1eVUwWUE2Sgo=" 31 | } 32 | }, 33 | "actions": [ 34 | { 35 | "action_id": "trivy", 36 | "params": { 37 | "config": { 38 | "threshold": "CRITICAL", 39 | "vuln_type": ["OS"] 40 | } 41 | } 42 | } 43 | ] 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /__tests__/resources/.vib/disfunctional-pipeline.json: -------------------------------------------------------------------------------- 1 | { 2 | "phases": { 3 | "package": { 4 | "context": { 5 | "resources": { 6 | "url": "github.com/bitnami/cha234rts/tarball/1bc9cb647ff60b2cf294cdd81a8ae29b32cd8703", 7 | "path": "/bitnami/word234press" 8 | } 9 | }, 10 | "actions": [ 11 | { 12 | "action_id": "action123" 13 | } 14 | ] 15 | }, 16 | "verify": { 17 | "context": { 18 | "resources": { 19 | "url": "github.com/bitnami/charts/tarball/1bc9cb647ff60b2cf294cdd81a8ae29b32cd8703", 20 | "path": "/bitnami/wordpress" 21 | }, 22 | "runtime_parameters": "d29yZHByZXNzVXNlcm5hbWU6IHRlc3RfdXNlcgp3b3JkcHJlc3NQYXNzd29yZDogQ29tcGxpY2F0ZWRQYXNzd29yZDEyMyE0CndvcmRwcmVzc0VtYWlsOiB0ZXN0X3VzZXJfZW1haWxAZW1haWwuY29tCndvcmRwcmVzc0ZpcnN0TmFtZTogVGVzdE5hbWUKd29yZHByZXNzTGFzdE5hbWU6IFRlc3RMYXN0TmFtZQp3b3JkcHJlc3NCbG9nTmFtZTogVGVzdF9Vc2VycydzIEJsb2chCnNtdHBIb3N0OiBtYWlsLnNlcnZlci5jb20Kc210cFBvcnQ6IDEyMApzbXRwVXNlcjogdGVzdF9tYWlsX3VzZXIKc210cFBhc3N3b3JkOiB0ZXN0X21haWxfcGFzc3dvcmQKbWFyaWFkYjoKICBhdXRoOgogICAgZGF0YWJhc2U6IHRlc3Rfd29yZHByZXNzX2RhdGFiYXNlCiAgICB1c2VybmFtZTogdGVzdF93b3JkcHJlc3NfdXNlcm5hbWUKICAgIHBhc3N3b3JkOiB0ZXN0X3dvcmRwcmVzc19wYXNzd29yZAp3b3JkcHJlc3NQbHVnaW5zOiBhbGwKY29udGFpbmVyU2VjdXJpdHlDb250ZXh0OgogIGVuYWJsZWQ6IHRydWUKICBydW5Bc1VzZXI6IDEwMDIKICBydW5Bc05vblJvb3Q6IHRydWUKd29yZHByZXNzVGFibGVQcmVmaXg6IHdvcmRwcmVzc18Kd29yZHByZXNzQXV0b1VwZGF0ZUxldmVsOiBtaW5vcg==", 23 | "application": { 24 | "kind": "HELM", 25 | "details": { 26 | "name": "wordpress", 27 | "version": "12.1.24", 28 | "repository": { 29 | "url": "https://charts.bitnami.com/bitnami" 30 | } 31 | } 32 | } 33 | }, 34 | "actions": [ 35 | { 36 | "action_id": "trivy", 37 | "params": { 38 | "threshold": "CRITICAL", 39 | "vuln_type": ["OS"] 40 | } 41 | } 42 | ] 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /__tests__/resources/.vib/pipeline-with-vib-envs.json: -------------------------------------------------------------------------------- 1 | { 2 | "phases": { 3 | "package": { 4 | "context": { 5 | "resources": { 6 | "url": "{VIB_ENV_URL}", 7 | "path": "{PATH}" 8 | } 9 | } 10 | }, 11 | "verify": { 12 | "actions": [ 13 | { 14 | "action_id": "ginkgo", 15 | "params": { 16 | "resources": { 17 | "path": "/.vib/metallb/ginkgo" 18 | }, 19 | "kubeconfig": "{{kubeconfig}}" 20 | } 21 | } 22 | ] 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /__tests__/resources/.vib/runtime-parameters-file.yaml: -------------------------------------------------------------------------------- 1 | wordpressUsername: test_user 2 | wordpressPassword: ComplicatedPassword123!4 3 | wordpressEmail: test_user_email@email.com 4 | wordpressFirstName: TestName 5 | wordpressLastName: TestLastName 6 | wordpressBlogName: Test_Users's Blog! 7 | smtpHost: mail.server.com 8 | smtpPort: 120 9 | smtpUser: test_mail_user 10 | smtpPassword: test_mail_password 11 | mariadb: 12 | auth: 13 | database: test_wordpress_database 14 | username: test_wordpress_username 15 | password: test_wordpress_password 16 | containerSecurityContext: 17 | enabled: true 18 | runAsUser: 1002 19 | runAsNonRoot: true 20 | wordpressTablePrefix: wordpress_ 21 | -------------------------------------------------------------------------------- /__tests__/resources/.vib/vib-pipeline-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "phases": { 3 | "package": { 4 | "context": { 5 | "resources": { 6 | "url": "https://github.com/bitnami/charts/tarball/d8a5f63aa65655f819bbd5d31f0be3c6c488e85c", 7 | "path": "/bitnami/wordpress" 8 | } 9 | }, 10 | "actions": [ 11 | { 12 | "action_id": "helm-lint" 13 | } 14 | ] 15 | }, 16 | "verify": { 17 | "context": { 18 | "resources": { 19 | "url": "https://github.com/bitnami/charts/tarball/d8a5f63aa65655f819bbd5d31f0be3c6c488e85c", 20 | "path": "/bitnami/wordpress" 21 | }, 22 | "application": { 23 | "kind": "HELM", 24 | "details": { 25 | "name": "wordpress", 26 | "version": "12.1.24", 27 | "repository": { 28 | "url": "https://charts.bitnami.com/bitnami" 29 | } 30 | }, 31 | "values": "d29yZHByZXNzUGFzc3dvcmQ6IFMzOUJLV2pTa2gKbWFyaWFkYjoKICBhdXRoOgogICAgcGFzc3dvcmQ6IFZxbDVSR2RjbzQKICAgIHJvb3RQYXNzd29yZDogVUM1eVUwWUE2Sgo=" 32 | } 33 | }, 34 | "actions": [ 35 | { 36 | "action_id": "trivy", 37 | "params": { 38 | "threshold": "CRITICAL", 39 | "vuln_type": ["OS"] 40 | } 41 | } 42 | ] 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /__tests__/resources/.vib/vib-pipeline-file.json: -------------------------------------------------------------------------------- 1 | { 2 | "phases": { 3 | "package": { 4 | "context": { 5 | "resources": { 6 | "url": "https://github.com/bitnami/charts/tarball/d8a5f63aa65655f819bbd5d31f0be3c6c488e85c", 7 | "path": "/bitnami/wordpress" 8 | } 9 | }, 10 | "actions": [ 11 | { 12 | "action_id": "helm-package" 13 | }, 14 | { 15 | "action_id": "helm-lint" 16 | } 17 | ] 18 | }, 19 | "verify": { 20 | "context": { 21 | "runtime_parameters": "" 22 | }, 23 | "actions": [ 24 | { 25 | "action_id": "trivy", 26 | "params": { 27 | "threshold": "CRITICAL", 28 | "vuln_type": ["OS"] 29 | } 30 | } 31 | ] 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /__tests__/resources/.vib/vib-pipeline.json: -------------------------------------------------------------------------------- 1 | { 2 | "phases": { 3 | "package": { 4 | "context": { 5 | "resources": { 6 | "url": "https://github.com/bitnami/charts/tarball/d8a5f63aa65655f819bbd5d31f0be3c6c488e85c", 7 | "path": "/bitnami/wordpress" 8 | } 9 | }, 10 | "actions": [ 11 | { 12 | "action_id": "helm-package" 13 | }, 14 | { 15 | "action_id": "helm-lint" 16 | } 17 | ] 18 | }, 19 | "verify": { 20 | "context": { 21 | "runtime_parameters": "d29yZHByZXNzVXNlcm5hbWU6IHRlc3RfdXNlcgp3b3JkcHJlc3NQYXNzd29yZDogQ29tcGxpY2F0ZWRQYXNzd29yZDEyMyE0CndvcmRwcmVzc0VtYWlsOiB0ZXN0X3VzZXJfZW1haWxAZW1haWwuY29tCndvcmRwcmVzc0ZpcnN0TmFtZTogVGVzdE5hbWUKd29yZHByZXNzTGFzdE5hbWU6IFRlc3RMYXN0TmFtZQp3b3JkcHJlc3NCbG9nTmFtZTogVGVzdF9Vc2VycydzIEJsb2chCnNtdHBIb3N0OiBtYWlsLnNlcnZlci5jb20Kc210cFBvcnQ6IDEyMApzbXRwVXNlcjogdGVzdF9tYWlsX3VzZXIKc210cFBhc3N3b3JkOiB0ZXN0X21haWxfcGFzc3dvcmQKbWFyaWFkYjoKICBhdXRoOgogICAgZGF0YWJhc2U6IHRlc3Rfd29yZHByZXNzX2RhdGFiYXNlCiAgICB1c2VybmFtZTogdGVzdF93b3JkcHJlc3NfdXNlcm5hbWUKICAgIHBhc3N3b3JkOiB0ZXN0X3dvcmRwcmVzc19wYXNzd29yZApjb250YWluZXJTZWN1cml0eUNvbnRleHQ6CiAgZW5hYmxlZDogdHJ1ZQogIHJ1bkFzVXNlcjogMTAwMgogIHJ1bkFzTm9uUm9vdDogdHJ1ZQp3b3JkcHJlc3NUYWJsZVByZWZpeDogd29yZHByZXNzXw==" 22 | }, 23 | "actions": [ 24 | { 25 | "action_id": "trivy", 26 | "params": { 27 | "threshold": "CRITICAL", 28 | "vuln_type": ["OS"] 29 | } 30 | } 31 | ] 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /__tests__/resources/.vib/vib-sha-archive.json: -------------------------------------------------------------------------------- 1 | { 2 | "phases": { 3 | "package": { 4 | "context": { 5 | "resources": { 6 | "url": "{SHA_ARCHIVE}", 7 | "path": "/bitnami/wordpress" 8 | } 9 | }, 10 | "actions": [ 11 | { 12 | "action_id": "helm-package" 13 | } 14 | ] 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /__tests__/resources/bundle-failed.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmware-labs/vmware-image-builder-action/8c9867d0afe7673f7efc4e80718f868f4955a123/__tests__/resources/bundle-failed.zip -------------------------------------------------------------------------------- /__tests__/resources/bundle-not-passed.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmware-labs/vmware-image-builder-action/8c9867d0afe7673f7efc4e80718f868f4955a123/__tests__/resources/bundle-not-passed.zip -------------------------------------------------------------------------------- /__tests__/resources/bundle.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmware-labs/vmware-image-builder-action/8c9867d0afe7673f7efc4e80718f868f4955a123/__tests__/resources/bundle.zip -------------------------------------------------------------------------------- /__tests__/resources/github-event-path-branch.json: -------------------------------------------------------------------------------- 1 | { 2 | "after": "ec6b2a241fcb7b5269c727d2227e86e02045c499", 3 | "base_ref": null, 4 | "before": "dbcda61031ddc4fc5230cf052b9b42e75d17ac15", 5 | "commits": [ 6 | { 7 | "author": { 8 | "email": "mpermar@gmail.com", 9 | "name": "mpermar", 10 | "username": "mpermar" 11 | }, 12 | "committer": { 13 | "email": "noreply@github.com", 14 | "name": "GitHub", 15 | "username": "web-flow" 16 | }, 17 | "distinct": true, 18 | "id": "ec6b2a241fcb7b5269c727d2227e86e02045c499", 19 | "message": "Update test.yml", 20 | "timestamp": "2022-06-16T10:04:20+02:00", 21 | "tree_id": "ed18ec575e45b7cafae8ea5d6a6133c2e747c0e1", 22 | "url": "https://github.com/mpermar/vib-action-test/commit/ec6b2a241fcb7b5269c727d2227e86e02045c499" 23 | } 24 | ], 25 | "compare": "https://github.com/mpermar/vib-action-test/compare/dbcda61031dd...ec6b2a241fcb", 26 | "created": false, 27 | "deleted": false, 28 | "forced": false, 29 | "head_commit": { 30 | "author": { 31 | "email": "mpermar@gmail.com", 32 | "name": "mpermar", 33 | "username": "mpermar" 34 | }, 35 | "committer": { 36 | "email": "noreply@github.com", 37 | "name": "GitHub", 38 | "username": "web-flow" 39 | }, 40 | "distinct": true, 41 | "id": "ec6b2a241fcb7b5269c727d2227e86e02045c499", 42 | "message": "Update test.yml", 43 | "timestamp": "2022-06-16T10:04:20+02:00", 44 | "tree_id": "ed18ec575e45b7cafae8ea5d6a6133c2e747c0e1", 45 | "url": "https://github.com/mpermar/vib-action-test/commit/ec6b2a241fcb7b5269c727d2227e86e02045c499" 46 | }, 47 | "pusher": { 48 | "email": "mpermar@gmail.com", 49 | "name": "mpermar" 50 | }, 51 | "ref": "refs/heads/mpermar-patch-2", 52 | "repository": { 53 | "allow_forking": true, 54 | "archive_url": "https://api.github.com/repos/mpermar/vib-action-test/{archive_format}{/ref}", 55 | "archived": false, 56 | "assignees_url": "https://api.github.com/repos/mpermar/vib-action-test/assignees{/user}", 57 | "blobs_url": "https://api.github.com/repos/mpermar/vib-action-test/git/blobs{/sha}", 58 | "branches_url": "https://api.github.com/repos/mpermar/vib-action-test/branches{/branch}", 59 | "clone_url": "https://github.com/mpermar/vib-action-test.git", 60 | "collaborators_url": "https://api.github.com/repos/mpermar/vib-action-test/collaborators{/collaborator}", 61 | "comments_url": "https://api.github.com/repos/mpermar/vib-action-test/comments{/number}", 62 | "commits_url": "https://api.github.com/repos/mpermar/vib-action-test/commits{/sha}", 63 | "compare_url": "https://api.github.com/repos/mpermar/vib-action-test/compare/{base}...{head}", 64 | "contents_url": "https://api.github.com/repos/mpermar/vib-action-test/contents/{+path}", 65 | "contributors_url": "https://api.github.com/repos/mpermar/vib-action-test/contributors", 66 | "created_at": 1640847119, 67 | "default_branch": "main", 68 | "deployments_url": "https://api.github.com/repos/mpermar/vib-action-test/deployments", 69 | "description": "A simple test for VMware Image Builder", 70 | "disabled": false, 71 | "downloads_url": "https://api.github.com/repos/mpermar/vib-action-test/downloads", 72 | "events_url": "https://api.github.com/repos/mpermar/vib-action-test/events", 73 | "fork": false, 74 | "forks": 0, 75 | "forks_count": 0, 76 | "forks_url": "https://api.github.com/repos/mpermar/vib-action-test/forks", 77 | "full_name": "mpermar/vib-action-test", 78 | "git_commits_url": "https://api.github.com/repos/mpermar/vib-action-test/git/commits{/sha}", 79 | "git_refs_url": "https://api.github.com/repos/mpermar/vib-action-test/git/refs{/sha}", 80 | "git_tags_url": "https://api.github.com/repos/mpermar/vib-action-test/git/tags{/sha}", 81 | "git_url": "git://github.com/mpermar/vib-action-test.git", 82 | "has_downloads": true, 83 | "has_issues": true, 84 | "has_pages": false, 85 | "has_projects": true, 86 | "has_wiki": true, 87 | "homepage": null, 88 | "hooks_url": "https://api.github.com/repos/mpermar/vib-action-test/hooks", 89 | "html_url": "https://github.com/mpermar/vib-action-test", 90 | "id": 442992403, 91 | "is_template": false, 92 | "issue_comment_url": "https://api.github.com/repos/mpermar/vib-action-test/issues/comments{/number}", 93 | "issue_events_url": "https://api.github.com/repos/mpermar/vib-action-test/issues/events{/number}", 94 | "issues_url": "https://api.github.com/repos/mpermar/vib-action-test/issues{/number}", 95 | "keys_url": "https://api.github.com/repos/mpermar/vib-action-test/keys{/key_id}", 96 | "labels_url": "https://api.github.com/repos/mpermar/vib-action-test/labels{/name}", 97 | "language": null, 98 | "languages_url": "https://api.github.com/repos/mpermar/vib-action-test/languages", 99 | "license": null, 100 | "master_branch": "main", 101 | "merges_url": "https://api.github.com/repos/mpermar/vib-action-test/merges", 102 | "milestones_url": "https://api.github.com/repos/mpermar/vib-action-test/milestones{/number}", 103 | "mirror_url": null, 104 | "name": "vib-action-test", 105 | "node_id": "R_kgDOGmeHEw", 106 | "notifications_url": "https://api.github.com/repos/mpermar/vib-action-test/notifications{?since,all,participating}", 107 | "open_issues": 0, 108 | "open_issues_count": 0, 109 | "owner": { 110 | "avatar_url": "https://avatars.githubusercontent.com/u/584642?v=4", 111 | "email": "mpermar@gmail.com", 112 | "events_url": "https://api.github.com/users/mpermar/events{/privacy}", 113 | "followers_url": "https://api.github.com/users/mpermar/followers", 114 | "following_url": "https://api.github.com/users/mpermar/following{/other_user}", 115 | "gists_url": "https://api.github.com/users/mpermar/gists{/gist_id}", 116 | "gravatar_id": "", 117 | "html_url": "https://github.com/mpermar", 118 | "id": 584642, 119 | "login": "mpermar", 120 | "name": "mpermar", 121 | "node_id": "MDQ6VXNlcjU4NDY0Mg==", 122 | "organizations_url": "https://api.github.com/users/mpermar/orgs", 123 | "received_events_url": "https://api.github.com/users/mpermar/received_events", 124 | "repos_url": "https://api.github.com/users/mpermar/repos", 125 | "site_admin": false, 126 | "starred_url": "https://api.github.com/users/mpermar/starred{/owner}{/repo}", 127 | "subscriptions_url": "https://api.github.com/users/mpermar/subscriptions", 128 | "type": "User", 129 | "url": "https://api.github.com/users/mpermar" 130 | }, 131 | "private": true, 132 | "pulls_url": "https://api.github.com/repos/mpermar/vib-action-test/pulls{/number}", 133 | "pushed_at": 1655366660, 134 | "releases_url": "https://api.github.com/repos/mpermar/vib-action-test/releases{/id}", 135 | "size": 37, 136 | "ssh_url": "git@github.com:mpermar/vib-action-test.git", 137 | "stargazers": 0, 138 | "stargazers_count": 0, 139 | "stargazers_url": "https://api.github.com/repos/mpermar/vib-action-test/stargazers", 140 | "statuses_url": "https://api.github.com/repos/mpermar/vib-action-test/statuses/{sha}", 141 | "subscribers_url": "https://api.github.com/repos/mpermar/vib-action-test/subscribers", 142 | "subscription_url": "https://api.github.com/repos/mpermar/vib-action-test/subscription", 143 | "svn_url": "https://github.com/mpermar/vib-action-test", 144 | "tags_url": "https://api.github.com/repos/mpermar/vib-action-test/tags", 145 | "teams_url": "https://api.github.com/repos/mpermar/vib-action-test/teams", 146 | "topics": [], 147 | "trees_url": "https://api.github.com/repos/mpermar/vib-action-test/git/trees{/sha}", 148 | "updated_at": "2022-02-16T21:29:42Z", 149 | "url": "https://github.com/mpermar/vib-action-test", 150 | "visibility": "private", 151 | "watchers": 0, 152 | "watchers_count": 0 153 | }, 154 | "sender": { 155 | "avatar_url": "https://avatars.githubusercontent.com/u/584642?v=4", 156 | "events_url": "https://api.github.com/users/mpermar/events{/privacy}", 157 | "followers_url": "https://api.github.com/users/mpermar/followers", 158 | "following_url": "https://api.github.com/users/mpermar/following{/other_user}", 159 | "gists_url": "https://api.github.com/users/mpermar/gists{/gist_id}", 160 | "gravatar_id": "", 161 | "html_url": "https://github.com/mpermar", 162 | "id": 584642, 163 | "login": "mpermar", 164 | "node_id": "MDQ6VXNlcjU4NDY0Mg==", 165 | "organizations_url": "https://api.github.com/users/mpermar/orgs", 166 | "received_events_url": "https://api.github.com/users/mpermar/received_events", 167 | "repos_url": "https://api.github.com/users/mpermar/repos", 168 | "site_admin": false, 169 | "starred_url": "https://api.github.com/users/mpermar/starred{/owner}{/repo}", 170 | "subscriptions_url": "https://api.github.com/users/mpermar/subscriptions", 171 | "type": "User", 172 | "url": "https://api.github.com/users/mpermar" 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /__tests__/resources/github-event-path.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": "reopened", 3 | "number": 4, 4 | "pull_request": { 5 | "_links": { 6 | "comments": { 7 | "href": "https://api.github.com/repos/mpermar/vib-action-test/issues/4/comments" 8 | }, 9 | "commits": { 10 | "href": "https://api.github.com/repos/mpermar/vib-action-test/pulls/4/commits" 11 | }, 12 | "html": { 13 | "href": "https://github.com/mpermar/vib-action-test/pull/4" 14 | }, 15 | "issue": { 16 | "href": "https://api.github.com/repos/mpermar/vib-action-test/issues/4" 17 | }, 18 | "review_comment": { 19 | "href": "https://api.github.com/repos/mpermar/vib-action-test/pulls/comments{/number}" 20 | }, 21 | "review_comments": { 22 | "href": "https://api.github.com/repos/mpermar/vib-action-test/pulls/4/comments" 23 | }, 24 | "self": { 25 | "href": "https://api.github.com/repos/mpermar/vib-action-test/pulls/4" 26 | }, 27 | "statuses": { 28 | "href": "https://api.github.com/repos/mpermar/vib-action-test/statuses/35f8f03e6bd0210fa6ee42d3af2eabc65b92f330" 29 | } 30 | }, 31 | "active_lock_reason": null, 32 | "additions": 2, 33 | "assignee": null, 34 | "assignees": [], 35 | "author_association": "OWNER", 36 | "auto_merge": null, 37 | "base": { 38 | "label": "mpermar:main", 39 | "ref": "main", 40 | "repo": { 41 | "allow_auto_merge": false, 42 | "allow_forking": true, 43 | "allow_merge_commit": true, 44 | "allow_rebase_merge": true, 45 | "allow_squash_merge": true, 46 | "allow_update_branch": false, 47 | "archive_url": "https://api.github.com/repos/mpermar/vib-action-test/{archive_format}{/ref}", 48 | "archived": false, 49 | "assignees_url": "https://api.github.com/repos/mpermar/vib-action-test/assignees{/user}", 50 | "blobs_url": "https://api.github.com/repos/mpermar/vib-action-test/git/blobs{/sha}", 51 | "branches_url": "https://api.github.com/repos/mpermar/vib-action-test/branches{/branch}", 52 | "clone_url": "https://github.com/mpermar/vib-action-test.git", 53 | "collaborators_url": "https://api.github.com/repos/mpermar/vib-action-test/collaborators{/collaborator}", 54 | "comments_url": "https://api.github.com/repos/mpermar/vib-action-test/comments{/number}", 55 | "commits_url": "https://api.github.com/repos/mpermar/vib-action-test/commits{/sha}", 56 | "compare_url": "https://api.github.com/repos/mpermar/vib-action-test/compare/{base}...{head}", 57 | "contents_url": "https://api.github.com/repos/mpermar/vib-action-test/contents/{+path}", 58 | "contributors_url": "https://api.github.com/repos/mpermar/vib-action-test/contributors", 59 | "created_at": "2021-12-30T06:51:59Z", 60 | "default_branch": "main", 61 | "delete_branch_on_merge": false, 62 | "deployments_url": "https://api.github.com/repos/mpermar/vib-action-test/deployments", 63 | "description": "A simple test for VMware Image Builder", 64 | "disabled": false, 65 | "downloads_url": "https://api.github.com/repos/mpermar/vib-action-test/downloads", 66 | "events_url": "https://api.github.com/repos/mpermar/vib-action-test/events", 67 | "fork": false, 68 | "forks": 1, 69 | "forks_count": 1, 70 | "forks_url": "https://api.github.com/repos/mpermar/vib-action-test/forks", 71 | "full_name": "mpermar/vib-action-test", 72 | "git_commits_url": "https://api.github.com/repos/mpermar/vib-action-test/git/commits{/sha}", 73 | "git_refs_url": "https://api.github.com/repos/mpermar/vib-action-test/git/refs{/sha}", 74 | "git_tags_url": "https://api.github.com/repos/mpermar/vib-action-test/git/tags{/sha}", 75 | "git_url": "git://github.com/mpermar/vib-action-test.git", 76 | "has_downloads": true, 77 | "has_issues": true, 78 | "has_pages": false, 79 | "has_projects": true, 80 | "has_wiki": true, 81 | "homepage": null, 82 | "hooks_url": "https://api.github.com/repos/mpermar/vib-action-test/hooks", 83 | "html_url": "https://github.com/mpermar/vib-action-test", 84 | "id": 442992403, 85 | "is_template": false, 86 | "issue_comment_url": "https://api.github.com/repos/mpermar/vib-action-test/issues/comments{/number}", 87 | "issue_events_url": "https://api.github.com/repos/mpermar/vib-action-test/issues/events{/number}", 88 | "issues_url": "https://api.github.com/repos/mpermar/vib-action-test/issues{/number}", 89 | "keys_url": "https://api.github.com/repos/mpermar/vib-action-test/keys{/key_id}", 90 | "labels_url": "https://api.github.com/repos/mpermar/vib-action-test/labels{/name}", 91 | "language": null, 92 | "languages_url": "https://api.github.com/repos/mpermar/vib-action-test/languages", 93 | "license": null, 94 | "merges_url": "https://api.github.com/repos/mpermar/vib-action-test/merges", 95 | "milestones_url": "https://api.github.com/repos/mpermar/vib-action-test/milestones{/number}", 96 | "mirror_url": null, 97 | "name": "vib-action-test", 98 | "node_id": "R_kgDOGmeHEw", 99 | "notifications_url": "https://api.github.com/repos/mpermar/vib-action-test/notifications{?since,all,participating}", 100 | "open_issues": 3, 101 | "open_issues_count": 3, 102 | "owner": { 103 | "avatar_url": "https://avatars.githubusercontent.com/u/584642?v=4", 104 | "events_url": "https://api.github.com/users/mpermar/events{/privacy}", 105 | "followers_url": "https://api.github.com/users/mpermar/followers", 106 | "following_url": "https://api.github.com/users/mpermar/following{/other_user}", 107 | "gists_url": "https://api.github.com/users/mpermar/gists{/gist_id}", 108 | "gravatar_id": "", 109 | "html_url": "https://github.com/mpermar", 110 | "id": 584642, 111 | "login": "mpermar", 112 | "node_id": "MDQ6VXNlcjU4NDY0Mg==", 113 | "organizations_url": "https://api.github.com/users/mpermar/orgs", 114 | "received_events_url": "https://api.github.com/users/mpermar/received_events", 115 | "repos_url": "https://api.github.com/users/mpermar/repos", 116 | "site_admin": false, 117 | "starred_url": "https://api.github.com/users/mpermar/starred{/owner}{/repo}", 118 | "subscriptions_url": "https://api.github.com/users/mpermar/subscriptions", 119 | "type": "User", 120 | "url": "https://api.github.com/users/mpermar" 121 | }, 122 | "private": false, 123 | "pulls_url": "https://api.github.com/repos/mpermar/vib-action-test/pulls{/number}", 124 | "pushed_at": "2022-02-01T13:51:28Z", 125 | "releases_url": "https://api.github.com/repos/mpermar/vib-action-test/releases{/id}", 126 | "size": 17, 127 | "ssh_url": "git@github.com:mpermar/vib-action-test.git", 128 | "stargazers_count": 0, 129 | "stargazers_url": "https://api.github.com/repos/mpermar/vib-action-test/stargazers", 130 | "statuses_url": "https://api.github.com/repos/mpermar/vib-action-test/statuses/{sha}", 131 | "subscribers_url": "https://api.github.com/repos/mpermar/vib-action-test/subscribers", 132 | "subscription_url": "https://api.github.com/repos/mpermar/vib-action-test/subscription", 133 | "svn_url": "https://github.com/mpermar/vib-action-test", 134 | "tags_url": "https://api.github.com/repos/mpermar/vib-action-test/tags", 135 | "teams_url": "https://api.github.com/repos/mpermar/vib-action-test/teams", 136 | "topics": [], 137 | "trees_url": "https://api.github.com/repos/mpermar/vib-action-test/git/trees{/sha}", 138 | "updated_at": "2022-01-27T15:20:38Z", 139 | "url": "https://api.github.com/repos/mpermar/vib-action-test", 140 | "visibility": "public", 141 | "watchers": 0, 142 | "watchers_count": 0 143 | }, 144 | "sha": "3bb4c06c914818c440b25e5c649728c186b72b85", 145 | "user": { 146 | "avatar_url": "https://avatars.githubusercontent.com/u/584642?v=4", 147 | "events_url": "https://api.github.com/users/mpermar/events{/privacy}", 148 | "followers_url": "https://api.github.com/users/mpermar/followers", 149 | "following_url": "https://api.github.com/users/mpermar/following{/other_user}", 150 | "gists_url": "https://api.github.com/users/mpermar/gists{/gist_id}", 151 | "gravatar_id": "", 152 | "html_url": "https://github.com/mpermar", 153 | "id": 584642, 154 | "login": "mpermar", 155 | "node_id": "MDQ6VXNlcjU4NDY0Mg==", 156 | "organizations_url": "https://api.github.com/users/mpermar/orgs", 157 | "received_events_url": "https://api.github.com/users/mpermar/received_events", 158 | "repos_url": "https://api.github.com/users/mpermar/repos", 159 | "site_admin": false, 160 | "starred_url": "https://api.github.com/users/mpermar/starred{/owner}{/repo}", 161 | "subscriptions_url": "https://api.github.com/users/mpermar/subscriptions", 162 | "type": "User", 163 | "url": "https://api.github.com/users/mpermar" 164 | } 165 | }, 166 | "body": null, 167 | "changed_files": 1, 168 | "closed_at": null, 169 | "comments": 0, 170 | "comments_url": "https://api.github.com/repos/mpermar/vib-action-test/issues/4/comments", 171 | "commits": 1, 172 | "commits_url": "https://api.github.com/repos/mpermar/vib-action-test/pulls/4/commits", 173 | "created_at": "2022-02-01T13:36:30Z", 174 | "deletions": 0, 175 | "diff_url": "https://github.com/mpermar/vib-action-test/pull/4.diff", 176 | "draft": false, 177 | "head": { 178 | "label": "mpermar:a-new-branch", 179 | "ref": "a-new-branch", 180 | "repo": { 181 | "allow_auto_merge": false, 182 | "allow_forking": true, 183 | "allow_merge_commit": true, 184 | "allow_rebase_merge": true, 185 | "allow_squash_merge": true, 186 | "allow_update_branch": false, 187 | "archive_url": "https://api.github.com/repos/mpermar/vib-action-test/{archive_format}{/ref}", 188 | "archived": false, 189 | "assignees_url": "https://api.github.com/repos/mpermar/vib-action-test/assignees{/user}", 190 | "blobs_url": "https://api.github.com/repos/mpermar/vib-action-test/git/blobs{/sha}", 191 | "branches_url": "https://api.github.com/repos/mpermar/vib-action-test/branches{/branch}", 192 | "clone_url": "https://github.com/mpermar/vib-action-test.git", 193 | "collaborators_url": "https://api.github.com/repos/mpermar/vib-action-test/collaborators{/collaborator}", 194 | "comments_url": "https://api.github.com/repos/mpermar/vib-action-test/comments{/number}", 195 | "commits_url": "https://api.github.com/repos/mpermar/vib-action-test/commits{/sha}", 196 | "compare_url": "https://api.github.com/repos/mpermar/vib-action-test/compare/{base}...{head}", 197 | "contents_url": "https://api.github.com/repos/mpermar/vib-action-test/contents/{+path}", 198 | "contributors_url": "https://api.github.com/repos/mpermar/vib-action-test/contributors", 199 | "created_at": "2021-12-30T06:51:59Z", 200 | "default_branch": "main", 201 | "delete_branch_on_merge": false, 202 | "deployments_url": "https://api.github.com/repos/mpermar/vib-action-test/deployments", 203 | "description": "A simple test for VMware Image Builder", 204 | "disabled": false, 205 | "downloads_url": "https://api.github.com/repos/mpermar/vib-action-test/downloads", 206 | "events_url": "https://api.github.com/repos/mpermar/vib-action-test/events", 207 | "fork": false, 208 | "forks": 1, 209 | "forks_count": 1, 210 | "forks_url": "https://api.github.com/repos/mpermar/vib-action-test/forks", 211 | "full_name": "mpermar/vib-action-test", 212 | "git_commits_url": "https://api.github.com/repos/mpermar/vib-action-test/git/commits{/sha}", 213 | "git_refs_url": "https://api.github.com/repos/mpermar/vib-action-test/git/refs{/sha}", 214 | "git_tags_url": "https://api.github.com/repos/mpermar/vib-action-test/git/tags{/sha}", 215 | "git_url": "git://github.com/mpermar/vib-action-test.git", 216 | "has_downloads": true, 217 | "has_issues": true, 218 | "has_pages": false, 219 | "has_projects": true, 220 | "has_wiki": true, 221 | "homepage": null, 222 | "hooks_url": "https://api.github.com/repos/mpermar/vib-action-test/hooks", 223 | "html_url": "https://github.com/mpermar/vib-action-test", 224 | "id": 442992403, 225 | "is_template": false, 226 | "issue_comment_url": "https://api.github.com/repos/mpermar/vib-action-test/issues/comments{/number}", 227 | "issue_events_url": "https://api.github.com/repos/mpermar/vib-action-test/issues/events{/number}", 228 | "issues_url": "https://api.github.com/repos/mpermar/vib-action-test/issues{/number}", 229 | "keys_url": "https://api.github.com/repos/mpermar/vib-action-test/keys{/key_id}", 230 | "labels_url": "https://api.github.com/repos/mpermar/vib-action-test/labels{/name}", 231 | "language": null, 232 | "languages_url": "https://api.github.com/repos/mpermar/vib-action-test/languages", 233 | "license": null, 234 | "merges_url": "https://api.github.com/repos/mpermar/vib-action-test/merges", 235 | "milestones_url": "https://api.github.com/repos/mpermar/vib-action-test/milestones{/number}", 236 | "mirror_url": null, 237 | "name": "vib-action-test", 238 | "node_id": "R_kgDOGmeHEw", 239 | "notifications_url": "https://api.github.com/repos/mpermar/vib-action-test/notifications{?since,all,participating}", 240 | "open_issues": 3, 241 | "open_issues_count": 3, 242 | "owner": { 243 | "avatar_url": "https://avatars.githubusercontent.com/u/584642?v=4", 244 | "events_url": "https://api.github.com/users/mpermar/events{/privacy}", 245 | "followers_url": "https://api.github.com/users/mpermar/followers", 246 | "following_url": "https://api.github.com/users/mpermar/following{/other_user}", 247 | "gists_url": "https://api.github.com/users/mpermar/gists{/gist_id}", 248 | "gravatar_id": "", 249 | "html_url": "https://github.com/mpermar", 250 | "id": 584642, 251 | "login": "mpermar", 252 | "node_id": "MDQ6VXNlcjU4NDY0Mg==", 253 | "organizations_url": "https://api.github.com/users/mpermar/orgs", 254 | "received_events_url": "https://api.github.com/users/mpermar/received_events", 255 | "repos_url": "https://api.github.com/users/mpermar/repos", 256 | "site_admin": false, 257 | "starred_url": "https://api.github.com/users/mpermar/starred{/owner}{/repo}", 258 | "subscriptions_url": "https://api.github.com/users/mpermar/subscriptions", 259 | "type": "User", 260 | "url": "https://api.github.com/users/mpermar" 261 | }, 262 | "private": false, 263 | "pulls_url": "https://api.github.com/repos/mpermar/vib-action-test/pulls{/number}", 264 | "pushed_at": "2022-02-01T13:51:28Z", 265 | "releases_url": "https://api.github.com/repos/mpermar/vib-action-test/releases{/id}", 266 | "size": 17, 267 | "ssh_url": "git@github.com:mpermar/vib-action-test.git", 268 | "stargazers_count": 0, 269 | "stargazers_url": "https://api.github.com/repos/mpermar/vib-action-test/stargazers", 270 | "statuses_url": "https://api.github.com/repos/mpermar/vib-action-test/statuses/{sha}", 271 | "subscribers_url": "https://api.github.com/repos/mpermar/vib-action-test/subscribers", 272 | "subscription_url": "https://api.github.com/repos/mpermar/vib-action-test/subscription", 273 | "svn_url": "https://github.com/mpermar/vib-action-test", 274 | "tags_url": "https://api.github.com/repos/mpermar/vib-action-test/tags", 275 | "teams_url": "https://api.github.com/repos/mpermar/vib-action-test/teams", 276 | "topics": [], 277 | "trees_url": "https://api.github.com/repos/mpermar/vib-action-test/git/trees{/sha}", 278 | "updated_at": "2022-01-27T15:20:38Z", 279 | "url": "https://api.github.com/repos/mpermar/vib-action-test", 280 | "visibility": "public", 281 | "watchers": 0, 282 | "watchers_count": 0 283 | }, 284 | "sha": "35f8f03e6bd0210fa6ee42d3af2eabc65b92f330", 285 | "user": { 286 | "avatar_url": "https://avatars.githubusercontent.com/u/584642?v=4", 287 | "events_url": "https://api.github.com/users/mpermar/events{/privacy}", 288 | "followers_url": "https://api.github.com/users/mpermar/followers", 289 | "following_url": "https://api.github.com/users/mpermar/following{/other_user}", 290 | "gists_url": "https://api.github.com/users/mpermar/gists{/gist_id}", 291 | "gravatar_id": "", 292 | "html_url": "https://github.com/mpermar", 293 | "id": 584642, 294 | "login": "mpermar", 295 | "node_id": "MDQ6VXNlcjU4NDY0Mg==", 296 | "organizations_url": "https://api.github.com/users/mpermar/orgs", 297 | "received_events_url": "https://api.github.com/users/mpermar/received_events", 298 | "repos_url": "https://api.github.com/users/mpermar/repos", 299 | "site_admin": false, 300 | "starred_url": "https://api.github.com/users/mpermar/starred{/owner}{/repo}", 301 | "subscriptions_url": "https://api.github.com/users/mpermar/subscriptions", 302 | "type": "User", 303 | "url": "https://api.github.com/users/mpermar" 304 | } 305 | }, 306 | "html_url": "https://github.com/mpermar/vib-action-test/pull/4", 307 | "id": 837229134, 308 | "issue_url": "https://api.github.com/repos/mpermar/vib-action-test/issues/4", 309 | "labels": [], 310 | "locked": false, 311 | "maintainer_can_modify": false, 312 | "merge_commit_sha": "5536dcdfda07485716e4b7a10575e04275e638dc", 313 | "mergeable": null, 314 | "mergeable_state": "unknown", 315 | "merged": false, 316 | "merged_at": null, 317 | "merged_by": null, 318 | "milestone": null, 319 | "node_id": "PR_kwDOGmeHE84x5xpO", 320 | "number": 4, 321 | "patch_url": "https://github.com/mpermar/vib-action-test/pull/4.patch", 322 | "rebaseable": null, 323 | "requested_reviewers": [], 324 | "requested_teams": [], 325 | "review_comment_url": "https://api.github.com/repos/mpermar/vib-action-test/pulls/comments{/number}", 326 | "review_comments": 0, 327 | "review_comments_url": "https://api.github.com/repos/mpermar/vib-action-test/pulls/4/comments", 328 | "state": "open", 329 | "statuses_url": "https://api.github.com/repos/mpermar/vib-action-test/statuses/35f8f03e6bd0210fa6ee42d3af2eabc65b92f330", 330 | "title": "Creating new branch", 331 | "updated_at": "2022-02-01T13:51:39Z", 332 | "url": "https://api.github.com/repos/mpermar/vib-action-test/pulls/4", 333 | "user": { 334 | "avatar_url": "https://avatars.githubusercontent.com/u/584642?v=4", 335 | "events_url": "https://api.github.com/users/mpermar/events{/privacy}", 336 | "followers_url": "https://api.github.com/users/mpermar/followers", 337 | "following_url": "https://api.github.com/users/mpermar/following{/other_user}", 338 | "gists_url": "https://api.github.com/users/mpermar/gists{/gist_id}", 339 | "gravatar_id": "", 340 | "html_url": "https://github.com/mpermar", 341 | "id": 584642, 342 | "login": "mpermar", 343 | "node_id": "MDQ6VXNlcjU4NDY0Mg==", 344 | "organizations_url": "https://api.github.com/users/mpermar/orgs", 345 | "received_events_url": "https://api.github.com/users/mpermar/received_events", 346 | "repos_url": "https://api.github.com/users/mpermar/repos", 347 | "site_admin": false, 348 | "starred_url": "https://api.github.com/users/mpermar/starred{/owner}{/repo}", 349 | "subscriptions_url": "https://api.github.com/users/mpermar/subscriptions", 350 | "type": "User", 351 | "url": "https://api.github.com/users/mpermar" 352 | } 353 | }, 354 | "repository": { 355 | "allow_forking": true, 356 | "archive_url": "https://api.github.com/repos/mpermar/vib-action-test/{archive_format}{/ref}", 357 | "archived": false, 358 | "assignees_url": "https://api.github.com/repos/mpermar/vib-action-test/assignees{/user}", 359 | "blobs_url": "https://api.github.com/repos/mpermar/vib-action-test/git/blobs{/sha}", 360 | "branches_url": "https://api.github.com/repos/mpermar/vib-action-test/branches{/branch}", 361 | "clone_url": "https://github.com/mpermar/vib-action-test.git", 362 | "collaborators_url": "https://api.github.com/repos/mpermar/vib-action-test/collaborators{/collaborator}", 363 | "comments_url": "https://api.github.com/repos/mpermar/vib-action-test/comments{/number}", 364 | "commits_url": "https://api.github.com/repos/mpermar/vib-action-test/commits{/sha}", 365 | "compare_url": "https://api.github.com/repos/mpermar/vib-action-test/compare/{base}...{head}", 366 | "contents_url": "https://api.github.com/repos/mpermar/vib-action-test/contents/{+path}", 367 | "contributors_url": "https://api.github.com/repos/mpermar/vib-action-test/contributors", 368 | "created_at": "2021-12-30T06:51:59Z", 369 | "default_branch": "main", 370 | "deployments_url": "https://api.github.com/repos/mpermar/vib-action-test/deployments", 371 | "description": "A simple test for VMware Image Builder", 372 | "disabled": false, 373 | "downloads_url": "https://api.github.com/repos/mpermar/vib-action-test/downloads", 374 | "events_url": "https://api.github.com/repos/mpermar/vib-action-test/events", 375 | "fork": false, 376 | "forks": 1, 377 | "forks_count": 1, 378 | "forks_url": "https://api.github.com/repos/mpermar/vib-action-test/forks", 379 | "full_name": "mpermar/vib-action-test", 380 | "git_commits_url": "https://api.github.com/repos/mpermar/vib-action-test/git/commits{/sha}", 381 | "git_refs_url": "https://api.github.com/repos/mpermar/vib-action-test/git/refs{/sha}", 382 | "git_tags_url": "https://api.github.com/repos/mpermar/vib-action-test/git/tags{/sha}", 383 | "git_url": "git://github.com/mpermar/vib-action-test.git", 384 | "has_downloads": true, 385 | "has_issues": true, 386 | "has_pages": false, 387 | "has_projects": true, 388 | "has_wiki": true, 389 | "homepage": null, 390 | "hooks_url": "https://api.github.com/repos/mpermar/vib-action-test/hooks", 391 | "html_url": "https://github.com/mpermar/vib-action-test", 392 | "id": 442992403, 393 | "is_template": false, 394 | "issue_comment_url": "https://api.github.com/repos/mpermar/vib-action-test/issues/comments{/number}", 395 | "issue_events_url": "https://api.github.com/repos/mpermar/vib-action-test/issues/events{/number}", 396 | "issues_url": "https://api.github.com/repos/mpermar/vib-action-test/issues{/number}", 397 | "keys_url": "https://api.github.com/repos/mpermar/vib-action-test/keys{/key_id}", 398 | "labels_url": "https://api.github.com/repos/mpermar/vib-action-test/labels{/name}", 399 | "language": null, 400 | "languages_url": "https://api.github.com/repos/mpermar/vib-action-test/languages", 401 | "license": null, 402 | "merges_url": "https://api.github.com/repos/mpermar/vib-action-test/merges", 403 | "milestones_url": "https://api.github.com/repos/mpermar/vib-action-test/milestones{/number}", 404 | "mirror_url": null, 405 | "name": "vib-action-test", 406 | "node_id": "R_kgDOGmeHEw", 407 | "notifications_url": "https://api.github.com/repos/mpermar/vib-action-test/notifications{?since,all,participating}", 408 | "open_issues": 3, 409 | "open_issues_count": 3, 410 | "owner": { 411 | "avatar_url": "https://avatars.githubusercontent.com/u/584642?v=4", 412 | "events_url": "https://api.github.com/users/mpermar/events{/privacy}", 413 | "followers_url": "https://api.github.com/users/mpermar/followers", 414 | "following_url": "https://api.github.com/users/mpermar/following{/other_user}", 415 | "gists_url": "https://api.github.com/users/mpermar/gists{/gist_id}", 416 | "gravatar_id": "", 417 | "html_url": "https://github.com/mpermar", 418 | "id": 584642, 419 | "login": "mpermar", 420 | "node_id": "MDQ6VXNlcjU4NDY0Mg==", 421 | "organizations_url": "https://api.github.com/users/mpermar/orgs", 422 | "received_events_url": "https://api.github.com/users/mpermar/received_events", 423 | "repos_url": "https://api.github.com/users/mpermar/repos", 424 | "site_admin": false, 425 | "starred_url": "https://api.github.com/users/mpermar/starred{/owner}{/repo}", 426 | "subscriptions_url": "https://api.github.com/users/mpermar/subscriptions", 427 | "type": "User", 428 | "url": "https://api.github.com/users/mpermar" 429 | }, 430 | "private": false, 431 | "pulls_url": "https://api.github.com/repos/mpermar/vib-action-test/pulls{/number}", 432 | "pushed_at": "2022-02-01T13:51:28Z", 433 | "releases_url": "https://api.github.com/repos/mpermar/vib-action-test/releases{/id}", 434 | "size": 17, 435 | "ssh_url": "git@github.com:mpermar/vib-action-test.git", 436 | "stargazers_count": 0, 437 | "stargazers_url": "https://api.github.com/repos/mpermar/vib-action-test/stargazers", 438 | "statuses_url": "https://api.github.com/repos/mpermar/vib-action-test/statuses/{sha}", 439 | "subscribers_url": "https://api.github.com/repos/mpermar/vib-action-test/subscribers", 440 | "subscription_url": "https://api.github.com/repos/mpermar/vib-action-test/subscription", 441 | "svn_url": "https://github.com/mpermar/vib-action-test", 442 | "tags_url": "https://api.github.com/repos/mpermar/vib-action-test/tags", 443 | "teams_url": "https://api.github.com/repos/mpermar/vib-action-test/teams", 444 | "topics": [], 445 | "trees_url": "https://api.github.com/repos/mpermar/vib-action-test/git/trees{/sha}", 446 | "updated_at": "2022-01-27T15:20:38Z", 447 | "url": "https://api.github.com/repos/mpermar/vib-action-test", 448 | "visibility": "public", 449 | "watchers": 0, 450 | "watchers_count": 0 451 | }, 452 | "sender": { 453 | "avatar_url": "https://avatars.githubusercontent.com/u/584642?v=4", 454 | "events_url": "https://api.github.com/users/mpermar/events{/privacy}", 455 | "followers_url": "https://api.github.com/users/mpermar/followers", 456 | "following_url": "https://api.github.com/users/mpermar/following{/other_user}", 457 | "gists_url": "https://api.github.com/users/mpermar/gists{/gist_id}", 458 | "gravatar_id": "", 459 | "html_url": "https://github.com/mpermar", 460 | "id": 584642, 461 | "login": "mpermar", 462 | "node_id": "MDQ6VXNlcjU4NDY0Mg==", 463 | "organizations_url": "https://api.github.com/users/mpermar/orgs", 464 | "received_events_url": "https://api.github.com/users/mpermar/received_events", 465 | "repos_url": "https://api.github.com/users/mpermar/repos", 466 | "site_admin": false, 467 | "starred_url": "https://api.github.com/users/mpermar/starred{/owner}{/repo}", 468 | "subscriptions_url": "https://api.github.com/users/mpermar/subscriptions", 469 | "type": "User", 470 | "url": "https://api.github.com/users/mpermar" 471 | } 472 | } -------------------------------------------------------------------------------- /__tests__/resources/github-event-scheduled.json: -------------------------------------------------------------------------------- 1 | { "schedule": "?/5 * * * *" } -------------------------------------------------------------------------------- /__tests__/src/action.it.test.ts: -------------------------------------------------------------------------------- 1 | import * as core from "@actions/core" 2 | import path from 'path' 3 | import Action from "../../src/action" 4 | import { TaskStatus } from '../../src/client/vib/api' 5 | import fs from 'fs' 6 | 7 | const TWO_MINUTES = 1200000 8 | 9 | jest.spyOn(core, 'setFailed').mockImplementation() 10 | 11 | describe('Given a VIB Action', () => { 12 | 13 | let action: Action 14 | 15 | beforeAll(() => { 16 | delete process.env["GITHUB_EVENT_PATH"] 17 | delete process.env["GITHUB_SHA"] 18 | delete process.env["GITHUB_REPOSITORY"] 19 | 20 | action = new Action(path.join(__dirname, '..')) 21 | action.config = { ...action.config, baseFolder: 'resources/.vib' } 22 | }) 23 | 24 | it('When it is executed then it returns the final ActionResult', async () => { 25 | const result = await action.main() 26 | 27 | expect(result.executionGraph.status).toBe(TaskStatus.Succeeded) 28 | expect(result.executionGraph.tasks.length).toBe(3) 29 | expect(result.executionGraphReport).toBeDefined 30 | expect(result.executionGraphReport?.passed).toBe(false) 31 | expect(result.executionGraphReport?.actions.length).toBe(1) 32 | expect(result.artifacts.length).toBe(12) 33 | result.artifacts.forEach(a => expect(fs.existsSync(a)).toBeFalsy()) 34 | }, TWO_MINUTES) 35 | 36 | it('When the execution graph times out then it fails', async () => { 37 | action.config = { ...action.config, executionGraphCheckInterval: 100, pipelineDurationMillis: 100 } 38 | 39 | await expect(action.main()).rejects.toThrowError(/^Pipeline .+ timed out\. Ending pipeline execution\.$/) 40 | }) 41 | }) -------------------------------------------------------------------------------- /__tests__/src/action.test.ts: -------------------------------------------------------------------------------- 1 | import * as core from "@actions/core" 2 | import * as artifact from "@actions/artifact" 3 | import * as executionGraphMother from '../mother/execution-graph' 4 | import * as executionGraphReportMother from '../mother/execution-graph-report' 5 | import * as pipelineMother from '../mother/pipeline' 6 | import * as targetPlatformMother from '../mother/target-platform' 7 | import * as taskMother from '../mother/task' 8 | import * as Fixtures from '../fixtures/fixtures' 9 | import moment from "moment" 10 | import path from 'path' 11 | import Action from '../../src/action' 12 | import { ExecutionGraph, Pipeline, SemanticValidationHint, SemanticValidationLevel, TaskStatus } from "../../src/client/vib/api" 13 | import fs from "fs" 14 | 15 | jest.mock('../../src/client/csp') 16 | jest.mock('../../src/client/vib') 17 | 18 | jest.spyOn(core, 'error') 19 | jest.spyOn(core, 'info') 20 | jest.spyOn(core, 'setFailed').mockImplementation() 21 | jest.spyOn(core, 'warning') 22 | 23 | const STARTING_ENV = process.env 24 | 25 | describe('Given an Action', () => { 26 | 27 | let action: Action 28 | 29 | beforeEach(() => { 30 | process.env = { ...STARTING_ENV, ACTIONS_RUNTIME_TOKEN: 'test-token' } 31 | 32 | delete process.env["GITHUB_EVENT_PATH"] 33 | delete process.env["GITHUB_JOB"] 34 | delete process.env['GITHUB_RUN_ATTEMPT'] 35 | delete process.env["GITHUB_SHA"] 36 | delete process.env["GITHUB_REPOSITORY"] 37 | 38 | action = new Action(path.join(__dirname, "..")) 39 | action.config = { 40 | ...action.config, 41 | baseFolder: 'resources/.vib', 42 | executionGraphCheckInterval: 500, 43 | pipelineDurationMillis: 2500, 44 | uploadArtifacts: true 45 | } 46 | 47 | jest.clearAllMocks() 48 | }) 49 | 50 | afterEach(() => { 51 | jest.clearAllMocks() 52 | }) 53 | 54 | describe('and a checkCSPTokenExpiration function', () => { 55 | it('When the expiration window narrower than the expiration days warning then it warns', async () => { 56 | action.config = { ...action.config, tokenExpirationDaysWarning: 10 } 57 | jest.spyOn(action.csp, 'checkTokenExpiration') 58 | .mockResolvedValue(moment().add(action.config.tokenExpirationDaysWarning - 1, 'days').unix()); 59 | 60 | await action.checkCSPTokenExpiration() 61 | 62 | expect(core.warning).toBeCalledTimes(1) 63 | expect(core.warning).toBeCalledWith(`CSP API token will expire in ${action.config.tokenExpirationDaysWarning - 2} days.`) 64 | }) 65 | 66 | it('When the expiration window greater than the expiration days warning then it does not warn', async () => { 67 | action.config = { ...action.config, tokenExpirationDaysWarning: 10 } 68 | jest.spyOn(action.csp, 'checkTokenExpiration') 69 | .mockResolvedValue(moment().add(action.config.tokenExpirationDaysWarning + 1, 'days').unix()); 70 | 71 | await action.checkCSPTokenExpiration() 72 | 73 | expect(core.warning).not.toHaveBeenCalled() 74 | }) 75 | }) 76 | 77 | describe('and a readPipeline function', () => { 78 | it('When the default config is used then it reads the pipeline', async () => { 79 | const pipeline = await action.readPipeline() 80 | 81 | expect(pipeline).toBeDefined() 82 | expect(pipeline.phases.package).toBeDefined() 83 | expect(pipeline.phases.package?.actions.length).toEqual(2) 84 | }) 85 | 86 | it('When a custom pipeline location is used then it reads the pipeline', async () => { 87 | action.config = { ...action.config, baseFolder: 'resources/.vib-other', pipeline: 'vib-pipeline-other.json' } 88 | 89 | const pipeline = await action.readPipeline() 90 | 91 | expect(pipeline).toBeDefined() 92 | expect(pipeline.phases.package).toBeDefined() 93 | expect(pipeline.phases.package?.actions.length).toEqual(1) 94 | }) 95 | 96 | it('When GitHub information is found but no SHA_ARCHIVE is declared then it does not template it', async () => { 97 | const shaArchive = 'https://github.com/repo/archive/main.zip' 98 | action.config = { ...action.config, shaArchive } 99 | 100 | const pipeline = await action.readPipeline() 101 | 102 | expect(pipeline).toBeDefined() 103 | expect(JSON.stringify(pipeline)).not.toContain(shaArchive) 104 | }) 105 | 106 | it('When GitHub information is found and SHA_ARCHIVE is declared then it templates it', async () => { 107 | const shaArchive = 'https://github.com/repo/archive/main.zip' 108 | action.config = { ...action.config, shaArchive, pipeline: 'vib-sha-archive.json' } 109 | 110 | const pipeline = await action.readPipeline() 111 | 112 | expect(pipeline).toBeDefined() 113 | expect(pipeline.phases.package?.context?.resources).toBeDefined() 114 | expect(pipeline.phases.package?.context?.resources?.url).toEqual(shaArchive) 115 | }) 116 | 117 | it('When GitHub information is not found and SHA_ARCHIVE is declared then it throws', async () => { 118 | const pipeline = 'vib-sha-archive.json' 119 | action.config = { ...action.config, pipeline } 120 | 121 | await expect(action.readPipeline()).rejects 122 | .toThrowError(`Pipeline ${pipeline} expects SHA_ARCHIVE variable but either GITHUB_REPOSITORY or GITHUB_SHA cannot be found on environment.`) 123 | }) 124 | 125 | it('When env vars with VIB_ENV_ prefix exist then it substitutes them', async () => { 126 | process.env.VIB_ENV_URL = "https://www.github.com/bitnami/charts" 127 | process.env.VIB_ENV_PATH = "/bitnami/wordpress" 128 | action.config = { ...action.config, pipeline: 'pipeline-with-vib-envs.json'} 129 | 130 | const pipeline = await action.readPipeline() 131 | 132 | expect(pipeline).toBeDefined() 133 | expect(pipeline.phases.package?.context?.resources).toBeDefined() 134 | expect(pipeline.phases.package?.context?.resources?.url).toEqual(process.env.VIB_ENV_URL) 135 | expect(pipeline.phases.package?.context?.resources?.path).toEqual(process.env.VIB_ENV_PATH) 136 | }) 137 | 138 | it('When no replacements for templated variables exist then it warns', async () => { 139 | action.config = { ...action.config, pipeline: 'pipeline-with-vib-envs.json'} 140 | 141 | const pipeline = await action.readPipeline() 142 | 143 | expect(pipeline).toBeDefined() 144 | expect(pipeline.phases.package?.context?.resources).toBeDefined() 145 | expect(pipeline.phases.package?.context?.resources?.url).toEqual('{VIB_ENV_URL}') 146 | expect(pipeline.phases.package?.context?.resources?.path).toEqual('{PATH}') 147 | expect(core.warning).toBeCalledTimes(2) 148 | }) 149 | 150 | it('When env vars with VIB_ENV_ prefix exist but not their templated var then it warns', async () => { 151 | process.env.VIB_ENV_URL = "https://www.github.com/bitnami/charts" 152 | process.env.VIB_ENV_PATH = "/bitnami/wordpress" 153 | 154 | const pipeline = await action.readPipeline() 155 | 156 | expect(pipeline).toBeDefined() 157 | expect(core.warning).toBeCalledTimes(2) 158 | }) 159 | 160 | it('When variables with {{ exist then it does not substitute them', async () => { 161 | action.config = { ...action.config, pipeline: 'pipeline-with-vib-envs.json'} 162 | 163 | const pipeline = await action.readPipeline() 164 | 165 | expect(pipeline).toBeDefined() 166 | expect(pipeline.phases.verify?.actions).toBeDefined() 167 | expect(pipeline.phases.verify?.actions.length).toEqual(1) 168 | expect(pipeline.phases.verify?.actions[0].params['kubeconfig']).toEqual("{{kubeconfig}}") 169 | }) 170 | 171 | it('When a runtime_parameters file is provided then they are added into the pipeline in base64', async () => { 172 | action.config = { ...action.config, pipeline: 'vib-pipeline-file.json', runtimeParametersFile: 'runtime-parameters-file.yaml' } 173 | 174 | const pipeline = await action.readPipeline() 175 | expect(pipeline).toBeDefined() 176 | expect(pipeline.phases.verify?.context?.runtime_parameters).toBeDefined() 177 | expect(pipeline.phases.verify?.context?.runtime_parameters).toBe(Buffer.from(Fixtures.runtimeParameters()).toString('base64') 178 | ); 179 | }); 180 | }) 181 | 182 | describe('and a runPipeline function', () => { 183 | it('When a valid pipeline is given then it is submitted and the related execution graph is returned', async () => { 184 | const pipeline: Pipeline = pipelineMother.valid() 185 | const executionGraph: ExecutionGraph = executionGraphMother.empty(undefined, TaskStatus.Succeeded) 186 | jest.spyOn(action.vib, 'validatePipeline').mockResolvedValue([]) 187 | jest.spyOn(action.vib, 'createPipeline').mockResolvedValue(executionGraph.execution_graph_id) 188 | jest.spyOn(action.vib, 'getExecutionGraph').mockResolvedValue(executionGraph) 189 | 190 | const result = await action.runPipeline(pipeline) 191 | 192 | expect(action.vib.validatePipeline).toHaveBeenCalledWith(pipeline) 193 | expect(action.vib.createPipeline).toHaveBeenCalledWith(pipeline, action.config.pipelineDurationMillis, action.config.verificationMode) 194 | expect(action.vib.getExecutionGraph).toHaveBeenCalledWith(executionGraph.execution_graph_id) 195 | expect(result).toEqual(executionGraph) 196 | }) 197 | 198 | it('When a wrong pipeline is given then it throws', async () => { 199 | const pipeline: Pipeline = pipelineMother.valid() 200 | const error = 'Random test error' 201 | jest.spyOn(action.vib, 'validatePipeline').mockRejectedValue(new Error(error)) 202 | 203 | await expect(action.runPipeline(pipeline)).rejects.toThrowError(error) 204 | expect(action.vib.validatePipeline).toHaveBeenCalledWith(pipeline) 205 | expect(action.vib.createPipeline).not.toBeCalled() 206 | expect(action.vib.getExecutionGraph).not.toBeCalled() 207 | }) 208 | 209 | it('When a pipeline has validation hints then they are printed out', async () => { 210 | const pipeline: Pipeline = pipelineMother.valid() 211 | const hints: SemanticValidationHint[] = [ 212 | {level: SemanticValidationLevel.Info, message: 'info message'}, 213 | {level: SemanticValidationLevel.Warning, message: 'warning message'}, 214 | {level: SemanticValidationLevel.Error, message: 'error message'}] 215 | jest.spyOn(action.vib, 'validatePipeline').mockResolvedValue(hints) 216 | jest.spyOn(action.vib, 'createPipeline').mockResolvedValue('') 217 | jest.spyOn(action.vib, 'getExecutionGraph').mockResolvedValue(executionGraphMother.empty()) 218 | 219 | await action.runPipeline(pipeline) 220 | 221 | expect(action.vib.validatePipeline).toHaveBeenCalledWith(pipeline) 222 | expect(core.info).toBeCalledWith('Got pipeline validation hint: ' + hints[0].message) 223 | expect(core.warning).toBeCalledWith('Got pipeline validation hint: ' + hints[1].message) 224 | expect(core.error).toBeCalledWith('Got pipeline validation hint: ' + hints[2].message) 225 | }) 226 | 227 | it('When the vib client calls fail then it propagates the errors', async () => { 228 | const pipeline: Pipeline = pipelineMother.valid() 229 | const executionGraphId = 'fakeId' 230 | const error = new Error(`Could not find execution graph with id ${executionGraphId}`) 231 | jest.spyOn(action.vib, 'validatePipeline').mockResolvedValue([]) 232 | jest.spyOn(action.vib, 'createPipeline').mockResolvedValue(executionGraphId) 233 | jest.spyOn(action.vib, 'getExecutionGraph').mockRejectedValue(error) 234 | 235 | await expect(action.runPipeline(pipeline)).rejects.toThrowError(error) 236 | expect(action.vib.validatePipeline).toHaveBeenCalledWith(pipeline) 237 | expect(action.vib.createPipeline).toHaveBeenCalledWith(pipeline, action.config.pipelineDurationMillis, action.config.verificationMode) 238 | expect(action.vib.getExecutionGraph).toHaveBeenCalledWith(executionGraphId) 239 | }) 240 | 241 | it('When the execution graph takes some time to complete then it polls until it finishes', async () => { 242 | const pipeline: Pipeline = pipelineMother.valid() 243 | const executionGraph: ExecutionGraph = executionGraphMother.empty(undefined, TaskStatus.Failed) 244 | jest.spyOn(action.vib, 'validatePipeline').mockResolvedValue([]) 245 | jest.spyOn(action.vib, 'createPipeline').mockResolvedValue(executionGraph.execution_graph_id) 246 | jest.spyOn(action.vib, 'getExecutionGraph') 247 | .mockResolvedValueOnce({ ...executionGraph, status: TaskStatus.InProgress }) 248 | .mockResolvedValue(executionGraph) 249 | 250 | const result = await action.runPipeline(pipeline) 251 | 252 | expect(action.vib.getExecutionGraph).toHaveBeenCalledTimes(2) 253 | expect(action.vib.getExecutionGraph).toHaveBeenCalledWith(executionGraph.execution_graph_id) 254 | expect(result).toEqual(executionGraph) 255 | }) 256 | 257 | it('When the execution graph takes longer than the pipeline duration then it throws', async () => { 258 | action.config = { ...action.config, pipelineDurationMillis: 750 } 259 | const pipeline: Pipeline = pipelineMother.valid() 260 | const executionGraph: ExecutionGraph = executionGraphMother.empty(undefined, TaskStatus.InProgress) 261 | jest.spyOn(action.vib, 'validatePipeline').mockResolvedValue([]) 262 | jest.spyOn(action.vib, 'createPipeline').mockResolvedValue(executionGraph.execution_graph_id) 263 | jest.spyOn(action.vib, 'getExecutionGraph').mockResolvedValue(executionGraph) 264 | 265 | await expect(action.runPipeline(pipeline)).rejects.toThrowError() 266 | expect(action.vib.getExecutionGraph).toHaveBeenCalledTimes(2) 267 | }); 268 | 269 | it('When some tasks fail during the execution check then a log is printed once', async () => { 270 | const cypressFailed = taskMother.cypress(undefined, TaskStatus.Failed) 271 | const deployFailed = taskMother.deployment(undefined, TaskStatus.Failed, cypressFailed.task_id) 272 | const cypressSkipped = taskMother.cypress(undefined, TaskStatus.Skipped) 273 | const executionGraph: ExecutionGraph = executionGraphMother.empty(undefined, TaskStatus.InProgress, [ deployFailed, cypressFailed, cypressSkipped ]) 274 | jest.spyOn(action.vib, 'validatePipeline').mockResolvedValue([]) 275 | jest.spyOn(action.vib, 'createPipeline').mockResolvedValue(executionGraph.execution_graph_id) 276 | jest.spyOn(action.vib, 'getExecutionGraph') 277 | .mockResolvedValueOnce(executionGraph) 278 | .mockResolvedValueOnce(executionGraph) 279 | .mockResolvedValueOnce({...executionGraph, status: TaskStatus.Failed}) 280 | 281 | await action.runPipeline(pipelineMother.valid()) 282 | 283 | expect(core.error).toBeCalledTimes(3) 284 | expect(core.error).toHaveBeenNthCalledWith(1, 'Task deployment (cypress) with ID 413e631d-0692-48de-ad4e-3962620b8f40 has failed. Error: undefined') 285 | expect(core.error).toHaveBeenNthCalledWith(2, 'Task cypress with ID d426abec-4d9e-44d1-b540-0448197d5651 has failed. Error: undefined') 286 | expect(core.error).toHaveBeenNthCalledWith(3, 'Task cypress with ID d426abec-4d9e-44d1-b540-0448197d5651 was skipped. Error: undefined') 287 | 288 | }) 289 | }) 290 | 291 | describe('and a processExecutionGraph function', () => { 292 | 293 | it('When an execution graph is provided then it returns the corresponding action result', async () => { 294 | const executionGraph = executionGraphMother.empty('f090b2dc-807a-49ae-8af5-051b0615bafc', undefined, [ taskMother.trivy() ]) 295 | const executionGraphReport = executionGraphReportMother.report() 296 | jest.spyOn(action.vib, 'getExecutionGraphBundle').mockResolvedValue(Fixtures.bundle()) 297 | 298 | const result = await action.processExecutionGraph(executionGraph) 299 | 300 | expect(result.baseDir).toContain('__tests__') 301 | expect(result.executionGraphReport).toEqual(executionGraphReport) 302 | expect(result.artifacts.length).toEqual(9) 303 | for (const a of result.artifacts) { 304 | expect(fs.existsSync(a)).toBeTruthy() 305 | } 306 | }) 307 | 308 | it('When the download of the bundle fails then it does not throw', async () => { 309 | const executionGraph = executionGraphMother.empty('f090b2dc-807a-49ae-8af5-051b0615bafc', TaskStatus.Succeeded) 310 | const error = new Error('fake bundle error test') 311 | jest.spyOn(action.vib, 'getExecutionGraphBundle').mockRejectedValue(error) 312 | 313 | await expect(action.processExecutionGraph(executionGraph)).resolves.not.toThrowError() 314 | expect(action.vib.getExecutionGraphBundle).toHaveBeenCalledTimes(3) 315 | expect(core.warning).toHaveBeenCalledWith(`Error downloading bundle files for execution graph ${executionGraph.execution_graph_id}, error: ${error}`) 316 | }) 317 | 318 | it('When a SUCCESSFUL execution graph is provided then it returns the execution graph report', async () => { 319 | const executionGraph = executionGraphMother.empty('f090b2dc-807a-49ae-8af5-051b0615bafc') 320 | const executionGraphReport = executionGraphReportMother.report() 321 | jest.spyOn(action.vib, 'getExecutionGraphBundle').mockResolvedValue(Fixtures.bundle()) 322 | 323 | const result = await action.processExecutionGraph(executionGraph) 324 | 325 | expect(action.vib.getExecutionGraphBundle).toHaveBeenCalledTimes(1) 326 | expect(action.vib.getExecutionGraphBundle).toHaveBeenCalledWith(executionGraph.execution_graph_id) 327 | expect(result.executionGraphReport).toEqual(executionGraphReport) 328 | }) 329 | 330 | it('When a non SUCCESSFUL execution graph is provided then it does not return the execution graph report', async () => { 331 | const executionGraph = executionGraphMother.empty('f090b2dc-807a-49ae-8af5-051b0615bafc', TaskStatus.Failed) 332 | jest.spyOn(action.vib, 'getExecutionGraphBundle').mockResolvedValue(Fixtures.executionGraphFailed()) 333 | 334 | const result = await action.processExecutionGraph(executionGraph) 335 | 336 | expect(result.executionGraphReport).toBeUndefined() 337 | }) 338 | 339 | it('When a non SUCCESSFUL execution graph is provided then the action fails', async () => { 340 | const executionGraph = executionGraphMother.empty('f090b2dc-807a-49ae-8af5-051b0615bafc', TaskStatus.Failed) 341 | jest.spyOn(action.vib, 'getExecutionGraphBundle').mockResolvedValue(Fixtures.executionGraphFailed()) 342 | 343 | await action.processExecutionGraph(executionGraph) 344 | 345 | expect(core.setFailed).toHaveBeenCalled() 346 | }) 347 | 348 | it('When a SUCCESSFUL execution graph that did not pass is provided then the action fails', async () => { 349 | const executionGraph = executionGraphMother.empty('f090b2dc-807a-49ae-8af5-051b0615bafc', TaskStatus.Succeeded) 350 | jest.spyOn(action.vib, 'getExecutionGraphBundle').mockResolvedValue(Fixtures.executionGraphNotPassed()) 351 | 352 | await action.processExecutionGraph(executionGraph) 353 | 354 | expect(core.setFailed).toHaveBeenCalled() 355 | }) 356 | 357 | }) 358 | 359 | describe('and an uploadArtifacts function', () => { 360 | it('When uploadArtifacts is false then it does not upload anything', async () => { 361 | action.config = { ...action.config, uploadArtifacts: false } 362 | jest.spyOn(action.vib, 'getTargetPlatform').mockRejectedValueOnce(new Error('Target Platform not found')) 363 | const artifactClient = artifactClientMock() 364 | jest.spyOn(artifactClient, 'uploadArtifact') 365 | jest.spyOn(action, 'createArtifactClient').mockReturnValue(artifactClient) 366 | 367 | await action.uploadArtifacts('testBaseDir', [ 'testBaseDir/testArtifact' ], 'test-execution-grahp-id') 368 | 369 | expect(artifactClient.uploadArtifact).not.toBeCalledWith() 370 | expect(core.info).toBeCalledWith('Artifacts will not be published.') 371 | }) 372 | 373 | it('When the target platform is not found then it uses the job name in the artifact name', async () => { 374 | const baseDir = 'testBaseDir' 375 | const artifacts = [ 'testBaseDir/testArtifact' ] 376 | const executionGraphId = 'test-execution-grahp-id' 377 | jest.spyOn(action.vib, 'getTargetPlatform').mockRejectedValueOnce(new Error('Target Platform not found')) 378 | const artifactClient = artifactClientMock() 379 | jest.spyOn(artifactClient, 'uploadArtifact') 380 | jest.spyOn(action, 'createArtifactClient').mockReturnValue(artifactClient) 381 | 382 | await action.uploadArtifacts(baseDir, artifacts, executionGraphId) 383 | 384 | expect(artifactClient.uploadArtifact).toHaveBeenCalledWith('assets-undefined-test-exe', artifacts, baseDir) 385 | }) 386 | 387 | it('When the target platform is found then it is used in the artifact name', async () => { 388 | const targetPlatform = targetPlatformMother.gke() 389 | action.config = { ...action.config, targetPlatform: targetPlatform.id} 390 | const baseDir = 'testBaseDir' 391 | const artifacts = [ 'testBaseDir/testArtifact' ] 392 | const executionGraphId = 'test-execution-grahp-id' 393 | jest.spyOn(action.vib, 'getTargetPlatform').mockResolvedValue(targetPlatform) 394 | const artifactClient = artifactClientMock() 395 | jest.spyOn(artifactClient, 'uploadArtifact') 396 | jest.spyOn(action, 'createArtifactClient').mockReturnValue(artifactClient) 397 | 398 | await action.uploadArtifacts(baseDir, artifacts, executionGraphId) 399 | 400 | expect(artifactClient.uploadArtifact).toHaveBeenCalledWith('assets-undefined-GKE-test-exe', artifacts, baseDir) 401 | }) 402 | 403 | it('When a GitHub run attempt exists then it is used in the artifact name', async () => { 404 | process.env.GITHUB_RUN_ATTEMPT = '2' 405 | const baseDir = 'testBaseDir' 406 | const artifacts = [ 'testBaseDir/testArtifact' ] 407 | const executionGraphId = 'test-execution-grahp-id' 408 | jest.spyOn(action.vib, 'getTargetPlatform').mockRejectedValueOnce(new Error('Target Platform not found')) 409 | const artifactClient = artifactClientMock() 410 | jest.spyOn(artifactClient, 'uploadArtifact') 411 | jest.spyOn(action, 'createArtifactClient').mockReturnValue(artifactClient) 412 | 413 | await action.uploadArtifacts(baseDir, artifacts, executionGraphId) 414 | 415 | expect(artifactClient.uploadArtifact).toHaveBeenCalledWith('assets-undefined_2-test-exe', artifacts, baseDir) 416 | }) 417 | 418 | it('When a GitHub run attempt <= 1 exists then it is not used in the artifact name', async () => { 419 | process.env.GITHUB_RUN_ATTEMPT = '1' 420 | const baseDir = 'testBaseDir' 421 | const artifacts = [ 'testBaseDir/testArtifact' ] 422 | const executionGraphId = 'test-execution-grahp-id' 423 | jest.spyOn(action.vib, 'getTargetPlatform').mockRejectedValueOnce(new Error('Target Platform not found')) 424 | const artifactClient = artifactClientMock() 425 | jest.spyOn(artifactClient, 'uploadArtifact') 426 | jest.spyOn(action, 'createArtifactClient').mockReturnValue(artifactClient) 427 | 428 | await action.uploadArtifacts(baseDir, artifacts, executionGraphId) 429 | 430 | expect(artifactClient.uploadArtifact).toHaveBeenCalledWith('assets-undefined-test-exe', artifacts, baseDir) 431 | }) 432 | }) 433 | 434 | describe('and a summarize function', () => { 435 | it('When an execution graph and report are provided then it displays the prettified report', () => { 436 | const executionGraph = executionGraphMother 437 | .empty(undefined, undefined, [taskMother.cypress(), taskMother.trivy(), taskMother.trivy(undefined, TaskStatus.Skipped)]) 438 | const executionGraphReport = executionGraphReportMother.report() 439 | executionGraphReport.passed = false 440 | 441 | action.summarize(executionGraph, {baseDir: '', artifacts: [], executionGraph, executionGraphReport }) 442 | 443 | expect(core.info).toHaveBeenCalledTimes(4) 444 | expect(core.info).toHaveBeenNthCalledWith(1, '\u001b[1mPipeline result: \u001b[31mfailed\u001b[39m\u001b[22m') 445 | expect(core.info).toHaveBeenNthCalledWith(2, '\u001b[1mtrivy action:\u001b[22m \u001b[31mfailed\u001b[39m » Vulnerabilities: 6 minimal, 5 low, 4 medium, 3 high, \u001b[1m2 critical\u001b[22m, 1 unknown') 446 | expect(core.info).toHaveBeenNthCalledWith(3, '\u001b[1mcypress action:\u001b[22m \u001b[32mpassed\u001b[39m » Tests: \u001b[1m\u001b[32m3 passed\u001b[39m\u001b[22m, \u001b[1m\u001b[33m2 skipped\u001b[39m\u001b[22m, \u001b[1m\u001b[31m1 failed\u001b[39m\u001b[22m') 447 | expect(core.info).toHaveBeenNthCalledWith(4, '\u001b[1mActions: \u001b[32m1 passed\u001b[39m, \u001b[33m1 skipped\u001b[39m, \u001b[31m1 failed\u001b[39m, 3 total\u001b[22m') 448 | }) 449 | }) 450 | }) 451 | 452 | function artifactClientMock(downloadPath = ''): artifact.ArtifactClient { 453 | return { 454 | uploadArtifact: () => new Promise(resolve => resolve({ })), 455 | downloadArtifact: () => new Promise(resolve => resolve({downloadPath})), 456 | listArtifacts: () => new Promise(resolve => resolve({artifacts: []})), 457 | getArtifact: () => new Promise(resolve => resolve({artifact: { id: 0, name: '', size: 0 }})), 458 | deleteArtifact: () => new Promise(resolve => resolve({id: 0})) 459 | } 460 | } -------------------------------------------------------------------------------- /__tests__/src/client/clients.test.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line filenames/match-regex 2 | import * as clients from "../../../src/client/clients" 3 | import * as core from "@actions/core" 4 | import MockAdapter from "axios-mock-adapter" 5 | import { randomUUID } from "crypto" 6 | import { AxiosInstance } from "axios" 7 | 8 | const STARTING_ENV = process.env 9 | 10 | describe("Given a custom client", () => { 11 | let client: AxiosInstance 12 | let serverStub: MockAdapter 13 | 14 | beforeAll(async () => { 15 | // mock all output so that there is less noise when running tests 16 | jest.spyOn(core, "info").mockImplementation(msg => console.log("::info:: " + msg)) 17 | jest.spyOn(core, "warning").mockImplementation(msg => console.log("::warning:: " + msg)) 18 | jest.spyOn(core, "debug").mockImplementation(msg => console.log("::debug:: " + msg)) 19 | jest.spyOn(core, "setFailed").mockImplementation() 20 | }) 21 | 22 | beforeEach(async () => { 23 | jest.resetModules() 24 | process.env = { ...STARTING_ENV } 25 | 26 | client = clients.newClient({timeout: 1000}, {retries: 3, backoffIntervals: [100, 200, 300]}) 27 | serverStub = new MockAdapter(client) 28 | }) 29 | 30 | it("When it does a regular request then it succeeds", async () => { 31 | const route = `/${randomUUID()}` 32 | const body = 'ok' 33 | serverStub.onGet(route).replyOnce(200, body) 34 | 35 | const response = await client.get(route) 36 | 37 | expect(response.status).toEqual(200) 38 | expect(response.data).toEqual(body) 39 | }) 40 | 41 | it("When it times out then it retries and fails", async () => { 42 | const route = `/${randomUUID()}` 43 | 44 | serverStub.onGet(route).timeout() 45 | 46 | await expect(client.get(route)).rejects.toThrow(new Error("Could not execute operation. Retried 3 times.")) 47 | expect(core.info).toHaveBeenCalledTimes(3) 48 | }) 49 | 50 | it("When it has a network error then it retries and fails", async () => { 51 | const route = `/${randomUUID()}` 52 | 53 | serverStub.onGet(route).networkError() 54 | 55 | await expect(client.get(route)).rejects.toThrow(new Error("Could not execute operation. Retried 3 times.")) 56 | expect(core.info).toHaveBeenCalledTimes(3) 57 | }) 58 | 59 | it("When it times out then it retries and recovers", async () => { 60 | const route = `/${randomUUID()}` 61 | 62 | serverStub.onGet(route).timeoutOnce() 63 | 64 | // Not sure if this can be done better with axios-mock-adapter. Request will timeout once and then 65 | // returns a 404 as we cannot mock a proper response ( adapter only supports one mock response per endpoint ) 66 | await expect(client.get(route)).rejects.toThrow('Request failed with status code 404') 67 | expect(core.info).toHaveBeenCalledTimes(1) 68 | }) 69 | 70 | it("When it has a network error then it retries and recovers", async () => { 71 | const route = `/${randomUUID()}` 72 | 73 | serverStub.onGet(route).networkErrorOnce() 74 | 75 | // Not sure if this can be done better with axios-mock-adapter. Request will error once and then 76 | // returns a 404 as we cannot mock a proper response ( adapter only supports one mock response per endpoint ) 77 | await expect(client.get(route)).rejects.toThrow('Request failed with status code 404') 78 | expect(core.info).toHaveBeenCalledTimes(1) 79 | }) 80 | 81 | it("When it gets unsuccessful responses then it retries for retriable codes", async () => { 82 | const route = `/${randomUUID()}` 83 | 84 | serverStub.onGet(route).reply(503, { error: "some-error-back" }) 85 | 86 | await expect(client.get(route)).rejects.toThrow(new Error("Could not execute operation. Retried 3 times.")) 87 | }) 88 | 89 | it("When it gets unsuccessful responses then it doesn't retry unhandled codes", async () => { 90 | const route = `/${randomUUID()}` 91 | const statusCode = 400 92 | 93 | serverStub.onGet(route).reply(statusCode, { error: "some-error-back" }) 94 | 95 | await expect(client.get(route)).rejects.toThrow(`Request failed with status code ${statusCode}`) 96 | }) 97 | 98 | it("When it receives a Retry-After header then it retries accordingly", async () => { 99 | const route = `/${randomUUID()}` 100 | const retryPeriod = 1 101 | 102 | serverStub.onGet(route).reply(503, { error: "some-error-back" }, { "Retry-After": retryPeriod }) 103 | 104 | await expect(client.get(route)).rejects.toThrow(new Error("Could not execute operation. Retried 3 times.")) 105 | expect(core.debug).toHaveBeenCalledWith(`Following server advice. Will retry after ${retryPeriod} seconds`) 106 | }) 107 | 108 | it("When it receives a bad Retry-After header then it is resilient", async () => { 109 | const route = `/${randomUUID()}` 110 | const retryPeriod = 'foo' 111 | 112 | serverStub.onGet(route).reply(503, { error: "some-error-back" }, { "Retry-After": retryPeriod }) 113 | 114 | await expect(client.get(route)).rejects.toThrow(new Error("Could not execute operation. Retried 3 times.")) 115 | expect(core.debug).toHaveBeenCalledWith(`Could not parse Retry-After header value ${retryPeriod}`) 116 | }) 117 | 118 | it("When it requests a bad URI then it times out", async () => { 119 | const badUriClient = clients.newClient( 120 | { 121 | baseURL: `http://foo-${randomUUID().toString()}.vmware.com`, // non-existing, it will fail. 122 | timeout: 100, 123 | headers: { "Content-Type": "application/x-www-form-urlencoded" }, 124 | }, 125 | { 126 | retries: 3, 127 | backoffIntervals: [100, 200, 500], 128 | retriableErrorCodes: ["ECONNABORTED", "ENOTFOUND", "ECONNREFUSED"], 129 | } 130 | ) 131 | 132 | await expect(badUriClient.get("/bar")).rejects.toThrow(new Error("Could not execute operation. Retried 3 times.")) 133 | expect(core.info).toHaveBeenCalledTimes(3) 134 | }) 135 | }) 136 | -------------------------------------------------------------------------------- /__tests__/src/client/csp.test.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line filenames/match-regex 2 | import * as core from "@actions/core" 3 | import MockAdapter from "axios-mock-adapter" 4 | import CSP from "../../../src/client/csp" 5 | 6 | describe("Given a CSP client", () => { 7 | let cspClient: CSP 8 | let clientStub: MockAdapter 9 | 10 | beforeAll(async () => { 11 | jest.spyOn(core, "info").mockImplementation(msg => console.log("::info:: " + msg)) 12 | jest.spyOn(core, "warning").mockImplementation(msg => console.log("::warning:: " + msg)) 13 | jest.spyOn(core, "debug").mockImplementation(msg => console.log("::debug:: " + msg)) 14 | jest.spyOn(core, "setFailed").mockImplementation() 15 | }) 16 | 17 | beforeEach(async () => { 18 | // Mock a token so it is not a requirement when running tests 19 | process.env["CSP_API_TOKEN"] = "foo" 20 | cspClient = new CSP(120000, 3, [500, 1000, 2000]) 21 | clientStub = new MockAdapter(cspClient.client) 22 | }) 23 | 24 | it("CSP token gets cached", async () => { 25 | const expectedToken = "h72827dd" 26 | clientStub 27 | .onPost("/csp/gateway/am/api/auth/api-tokens/authorize") 28 | .replyOnce( 29 | 200, 30 | `{"id_token": "aToken","token_type": "bearer","expires_in": 1000,"scope": "*","access_token": "${expectedToken}","refresh_token": "aT4epjdh"}` 31 | ) 32 | clientStub 33 | .onPost("/csp/gateway/am/api/auth/api-tokens/authorize") 34 | .replyOnce( 35 | 200, 36 | `{"id_token": "aToken","token_type": "bearer","expires_in": 1000,"scope": "*","access_token": "token2","refresh_token": "aT4epjdh"}` 37 | ) 38 | 39 | const apiToken = await cspClient.getToken() 40 | expect(apiToken).toEqual(expectedToken) 41 | // Call again and our action should use the cached CSP token 42 | const apiToken2 = await cspClient.getToken() 43 | expect(apiToken2).toEqual(apiToken) 44 | }) 45 | 46 | it("CSP token to be refreshed", async () => { 47 | clientStub 48 | .onPost("/csp/gateway/am/api/auth/api-tokens/authorize") 49 | .replyOnce( 50 | 200, 51 | `{"id_token": "aToken","token_type": "bearer","expires_in": 1000,"scope": "*","access_token": "token1","refresh_token": "aT4epjdh"}` 52 | ) 53 | clientStub 54 | .onPost("/csp/gateway/am/api/auth/api-tokens/authorize") 55 | .replyOnce( 56 | 200, 57 | `{"id_token": "aToken","token_type": "bearer","expires_in": 1000,"scope": "*","access_token": "token2","refresh_token": "aT4epjdh"}` 58 | ) 59 | 60 | const apiToken = await cspClient.getToken(1) // token will expire after 1ms 61 | expect(apiToken).toBeDefined() 62 | 63 | await new Promise(resolve => setTimeout(resolve, 10)) 64 | 65 | // earlier token should have expired 66 | const apiToken2 = await cspClient.getToken() 67 | expect(apiToken2).not.toEqual(apiToken) 68 | }) 69 | 70 | it("No CSP_API_TOKEN throws an error", async () => { 71 | delete process.env["CSP_API_TOKEN"] 72 | await expect(cspClient.getToken()).rejects.toThrow(Error("CSP_API_TOKEN secret not found.")) 73 | }) 74 | 75 | it("No CSP_API_TOKEN throws an error when checking expiration date", async () => { 76 | delete process.env["CSP_API_TOKEN"] 77 | await expect(cspClient.checkTokenExpiration()).rejects.toThrow(Error("CSP_API_TOKEN secret not found.")) 78 | }) 79 | }) 80 | -------------------------------------------------------------------------------- /__tests__/src/client/vib.test.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line filenames/match-regex 2 | import { newClient } from "../../../src/client/clients" 3 | import VIB from "../../../src/client/vib" 4 | import { ExecutionGraphsApi, PipelinesApi, TargetPlatformsApi } from "../../../src/client/vib/api" 5 | 6 | jest.mock('../../../src/client/vib/api') 7 | jest.mock('../../../src/client/clients', () => { 8 | return { 9 | __esModule: true, 10 | newClient: jest.fn(() => 'mock client') 11 | } 12 | }) 13 | 14 | describe('Given a VIB client', () => { 15 | 16 | it('When it is initialized then it configures the underlying clients properly', () => { 17 | const timeout = 1000 18 | const retryCount = 2 19 | const retryIntervals = [50, 100] 20 | const userAgent = 'jest' 21 | 22 | new VIB(timeout, retryCount, retryIntervals, userAgent, undefined) 23 | 24 | expect(newClient).toHaveBeenCalledWith( 25 | { 26 | timeout: timeout, 27 | headers: { 28 | "Content-Type": "application/json", 29 | "User-Agent": `vib-action/${userAgent}`, 30 | }, 31 | }, 32 | { 33 | retries: retryCount, 34 | backoffIntervals: retryIntervals, 35 | }) 36 | expect(ExecutionGraphsApi).toHaveBeenCalledWith(undefined, undefined, 'mock client') 37 | expect(PipelinesApi).toHaveBeenCalledWith(undefined, undefined, 'mock client') 38 | expect(TargetPlatformsApi).toHaveBeenCalledWith(undefined, undefined, 'mock client') 39 | }) 40 | }) -------------------------------------------------------------------------------- /__tests__/src/config.test.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line filenames/match-regex 2 | import * as core from "@actions/core" 3 | import * as path from "path" 4 | import ConfigurationFactory from "../../src/config" 5 | 6 | const STARTING_ENV = process.env 7 | const root = path.join(__dirname, "..") 8 | const configFactory = new ConfigurationFactory(root) 9 | 10 | describe("Given a configuration", () => { 11 | beforeAll(() => { 12 | jest.spyOn(core, "info").mockImplementation(msg => console.log("::info:: " + msg)) 13 | jest.spyOn(core, "warning").mockImplementation(msg => console.log("::warning:: " + msg)) 14 | jest.spyOn(core, "debug").mockImplementation(msg => console.log("::debug:: " + msg)) 15 | jest.spyOn(core, "setFailed").mockImplementation() 16 | }) 17 | 18 | beforeEach(() => { 19 | process.env = { ...STARTING_ENV } 20 | 21 | // Needed to delete these for running tests on GitHub Action 22 | delete process.env["GITHUB_EVENT_PATH"] 23 | delete process.env["GITHUB_SHA"] 24 | delete process.env["GITHUB_REPOSITORY"] 25 | }) 26 | 27 | it("When github sha is not present there will be no sha archive config property", () => { 28 | const config = configFactory.getConfiguration() 29 | 30 | expect(config.shaArchive).toBeUndefined() 31 | }) 32 | 33 | it("When github repository is not present there will be no sha archive config property", () => { 34 | process.env.GITHUB_SHA = "aacf48f14ed73e4b368ab66abf4742b0e9afae54" 35 | 36 | const config = configFactory.getConfiguration() 37 | 38 | expect(config.shaArchive).toBeUndefined() 39 | }) 40 | 41 | it("When both github sha and repository are present then there will be sha archive config property set", () => { 42 | process.env.GITHUB_SHA = "aacf48f14ed73e4b368ab66abf4742b0e9afae54" 43 | process.env.GITHUB_REPOSITORY = "vmware/vib-action" 44 | 45 | const config = configFactory.getConfiguration() 46 | 47 | expect(config.shaArchive).toBeDefined() 48 | expect(config.shaArchive).toEqual( 49 | `https://github.com/vmware/vib-action/archive/aacf48f14ed73e4b368ab66abf4742b0e9afae54.zip` 50 | ) 51 | }) 52 | 53 | it("Loads event configuration from the environment path", () => { 54 | process.env.GITHUB_EVENT_PATH = path.join(root, "resources", "github-event-path.json") 55 | 56 | const config = configFactory.getConfiguration() 57 | 58 | expect(config.shaArchive).toBeDefined() 59 | expect(config.shaArchive).toBe("https://api.github.com/repos/mpermar/vib-action-test/tarball/a-new-branch") 60 | }) 61 | 62 | it("When event configuration exists SHA archive variable is set from its data", () => { 63 | process.env.GITHUB_SHA = "aacf48f14ed73e4b368ab66abf4742b0e9afae54" 64 | process.env.GITHUB_REPOSITORY = "vmware/vib-action" 65 | process.env.GITHUB_EVENT_PATH = path.join(root, "resources", "github-event-path.json") // overseeds the previous two env vars 66 | 67 | const config = configFactory.getConfiguration() 68 | expect(config.shaArchive).toBeDefined() 69 | expect(config.shaArchive).toEqual("https://api.github.com/repos/mpermar/vib-action-test/tarball/a-new-branch") 70 | 71 | }) 72 | 73 | it("When push from branch and no SHA archive variable is set then sha is picked from ref env", () => { 74 | process.env.GITHUB_REF_NAME = "martinpe-patch-1" // this is what rules 75 | process.env.GITHUB_EVENT_PATH = path.join(root, "resources", "github-event-path-branch.json") // still will use env var above 76 | 77 | const config = configFactory.getConfiguration() 78 | 79 | expect(config.shaArchive).toBeDefined() 80 | expect(config.shaArchive).toEqual("https://github.com/mpermar/vib-action-test/tarball/martinpe-patch-1") 81 | }) 82 | 83 | it("When a special character present in URL from 'tarball' onwards, GitHub Action encodes it", () => { 84 | process.env.GITHUB_REF_NAME = "#artine-patch-1" // this is what rules 85 | process.env.GITHUB_EVENT_PATH = path.join(root, "resources", "github-event-path-branch.json") // still will use env var above 86 | 87 | const config = configFactory.getConfiguration() 88 | 89 | expect(config.shaArchive).toBeDefined() 90 | expect(config.shaArchive).toContain('https://github.com/mpermar/vib-action-test/tarball/%23artine-patch-1') 91 | }) 92 | 93 | it("When push from branch and both SHA archive and REF are set then sha is picked from SHA env", () => { 94 | process.env.GITHUB_SHA = "aacf48f14ed73e4b368ab66abf4742b0e9afae54" // this will be ignored 95 | process.env.GITHUB_REF_NAME = "martinpe-patch-1" // this is what rules 96 | process.env.GITHUB_EVENT_PATH = path.join(root, "resources", "github-event-path-branch.json") // still will use env var above 97 | 98 | const config = configFactory.getConfiguration() 99 | 100 | expect(config.shaArchive).toBeDefined() 101 | expect(config.shaArchive).toEqual( 102 | "https://github.com/mpermar/vib-action-test/tarball/aacf48f14ed73e4b368ab66abf4742b0e9afae54" 103 | ) 104 | }) 105 | 106 | it("When triggered from a scheduled job, GitHub Action still gets an archive to download", () => { 107 | process.env.GITHUB_REPOSITORY = "vmware/vib-action" 108 | process.env.GITHUB_SERVER_URL = "https://github.com" 109 | process.env.GITHUB_REF_NAME = "martinpe-patch-1" 110 | process.env.GITHUB_EVENT_PATH = path.join(root, "resources", "github-event-scheduled.json") 111 | 112 | const config = configFactory.getConfiguration() 113 | 114 | expect(config.shaArchive).toBeDefined() 115 | expect(config.shaArchive).toEqual("https://github.com/vmware/vib-action/tarball/martinpe-patch-1") 116 | }) 117 | 118 | it("Default base folder is used when not customized", () => { 119 | const config = configFactory.getConfiguration() 120 | 121 | expect(config.baseFolder).toEqual(".vib") 122 | }) 123 | 124 | it("Default base folder is not used when customized", () => { 125 | const expectedInputconfig = ".vib-other" 126 | process.env["INPUT_CONFIG"] = expectedInputconfig 127 | 128 | const config = configFactory.getConfiguration() 129 | 130 | expect(config.baseFolder).toEqual(expectedInputconfig) 131 | }) 132 | 133 | it("Default pipeline is used when not customized", () => { 134 | const config = configFactory.getConfiguration() 135 | 136 | expect(config.pipeline).toEqual("vib-pipeline.json") 137 | }) 138 | 139 | it("Default pipeline duration is used when not customized", () => { 140 | const config = configFactory.getConfiguration() 141 | 142 | expect(config.pipelineDurationMillis).toEqual(90 * 60 * 1000) 143 | }) 144 | 145 | it("Passed pipeline duration is used when customized", () => { 146 | const expectedMaxDuration = 3333 147 | process.env["INPUT_MAX-PIPELINE-DURATION"] = "" + expectedMaxDuration 148 | 149 | const config = configFactory.getConfiguration() 150 | 151 | expect(config.pipelineDurationMillis).toEqual(expectedMaxDuration * 1000) 152 | }) 153 | 154 | it("If file does not exist, throw an error", () => { 155 | process.env["INPUT_PIPELINE"] = "wrong.json" 156 | 157 | configFactory.getConfiguration() 158 | 159 | expect(core.setFailed).toHaveBeenCalledWith(expect.stringContaining("Could not find pipeline")) 160 | }) 161 | 162 | it("If verification mode has not a valid value the default is used", () => { 163 | const wrongVerificationMode = "WHATEVER" 164 | process.env["INPUT_VERIFICATION-MODE"] = wrongVerificationMode 165 | 166 | configFactory.getConfiguration() 167 | 168 | expect(core.warning).toHaveBeenCalledWith( 169 | `The value ${wrongVerificationMode} for verification-mode is not valid, the default value will be used.` 170 | ) 171 | }) 172 | 173 | it("Passed verification mode is used when customized", () => { 174 | const expectedVerificationMode = "SERIAL" 175 | process.env["INPUT_VERIFICATION-MODE"] = expectedVerificationMode 176 | 177 | const config = configFactory.getConfiguration() 178 | 179 | expect(config.verificationMode.toString()).toEqual(expectedVerificationMode) 180 | }) 181 | }) 182 | -------------------------------------------------------------------------------- /__tests__/src/sanitize.test.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line filenames/match-regex 2 | import { sanitize } from "../../src/sanitize" 3 | 4 | describe("Sanitize", () => { 5 | it("Gets a container URI and generates a valid filename with underscores", async () => { 6 | const sanitized = sanitize("docker.io/bitnami/mariadb:10.5.13-debian-10-r0", "_") 7 | expect(sanitized).toEqual("docker.io_bitnami_mariadb_10.5.13-debian-10-r0") 8 | }) 9 | 10 | it("Gets a container URI and generates a valid filename with dashes", async () => { 11 | const sanitized = sanitize("docker.io/bitnami/mariadb:10.5.13-debian-10-r0", "-") 12 | expect(sanitized).toEqual("docker.io-bitnami-mariadb-10.5.13-debian-10-r0") 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'VMware Image Builder' 2 | description: 'VMware Image Builder packages, verifies and publishes cloud native Open Source Software.' 3 | inputs: 4 | config: 5 | description: 'Location of the VIB pipeline and all related content, eg. Cypress tests, jmeter configuration, etc.' 6 | required: false 7 | default: '.vib' 8 | path: 9 | description: 'The location of the content that needs to be processed by VIB, e.g. Helm chart, Carvel package, etc.' 10 | require: false 11 | default: '/' 12 | pipeline: 13 | description: 'Pipeline that will be run through VIB. This path is relative to the config folder.' 14 | required: false 15 | default: 'vib-pipeline.json' 16 | upload-artifacts: 17 | description: 'Specifies whether the GitHub Action will publish logs and reports as GitHub artifacts.' 18 | required: false 19 | default: true 20 | retry-count: 21 | description: 'Number of retries to do in case of failure reaching out to VIB.' 22 | required: false 23 | default: 3 24 | backoff-intervals: 25 | description: 'Integer or array of integers that define the backoff intervals. When providing an integer, all retries will back off for the same amount of time. When input is an array the action will try to use the corresponding backoff time for the given retry, e.g. first time 5 seconds, second time 10 seconds, etc. If the array is shorter than the number of retries, then the last backoff interval will be used for overflowing attempts.' 26 | required: false 27 | default: '[5000, 10000, 15000]' 28 | only-upload-on-failure: 29 | description: It sets whether the GitHub Action should upload artifacts for every task or only for those tasks that have failed. 30 | required: false 31 | default: true 32 | http-timeout: 33 | description: 'Number of seconds the GitHub Action waits for an HTTP timeout before failing.' 34 | required: false 35 | default: '120000' 36 | verification-mode: 37 | description: 'When specified, this GitHub Action will request VIB to execute the pipeline in the requested verification mode. Possible values: SERIAL, PARALLEL.' 38 | required: false 39 | default: PARALLEL 40 | max-pipeline-duration: 41 | description: 'Maximum time for a pipeline execution to be completed. The value should be in seconds.' 42 | required: false 43 | default: '5400' 44 | execution-graph-check-interval: 45 | description: 'Interval between execution graph state checks. The value shoud be in seconds.' 46 | required: false 47 | default: '30' 48 | runtime-parameters-file: 49 | description: 'File with the runtime parameters' 50 | required: false 51 | outputs: 52 | execution-graph: 53 | description: 'Execution graph result from submitting the pipeline.' 54 | result: 55 | description: 'The resulting report from the execution graph with tasks executed and their statuses.' 56 | runs: 57 | using: 'node20' 58 | main: 'dist/index.js' 59 | branding: 60 | icon: 'command' 61 | color: 'green' 62 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | moduleFileExtensions: ['js', 'ts'], 4 | roots: [''], 5 | testEnvironment: 'node', 6 | testMatch: ['**/*.test.ts'], 7 | testRunner: 'jest-circus/runner', 8 | testTimeout: 15000, 9 | transform: { 10 | '^.+\\.ts$': 'ts-jest' 11 | }, 12 | verbose: true 13 | } 14 | -------------------------------------------------------------------------------- /openapitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json", 3 | "spaces": 2, 4 | "generator-cli": { 5 | "version": "6.2.1", 6 | "generators": { 7 | "vib": { 8 | "generatorName": "typescript-axios", 9 | "inputSpec": "https://cp.bromelia.vmware.com/v1/api.yml", 10 | "output": "src/client/vib", 11 | "additionalProperties": { 12 | "usePromises": true 13 | } 14 | } 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vmware-image-builder-action", 3 | "version": "0.10.0", 4 | "private": true, 5 | "description": "VMware Image Builder GitHub Action", 6 | "main": "lib/index.js", 7 | "scripts": { 8 | "all": "npm run build && npm run format && npm run package && npm test", 9 | "build": "tsc", 10 | "format": "eslint --fix \"src/**/*.ts\"", 11 | "generate-vib-client": "openapi-generator-cli generate -c openapitools.json --generator-key vib", 12 | "lint": "eslint \"src/**/*.ts\"", 13 | "package": "ncc build --source-map --license licenses.txt", 14 | "preversion": "npm ci && npm run build && npm run package", 15 | "postversion": "git push && git push --tags", 16 | "postinstall": "npm run generate-vib-client", 17 | "test": "jest --detectOpenHandles" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/vmware-labs/vmware-image-builder-action.git" 22 | }, 23 | "keywords": [ 24 | "actions", 25 | "node", 26 | "setup" 27 | ], 28 | "author": "", 29 | "license": "BSD-2", 30 | "dependencies": { 31 | "@actions/artifact": "^2.1.11", 32 | "@actions/core": "^1.11.1", 33 | "@actions/glob": "^0.5.0", 34 | "@actions/io": "^1.1.3", 35 | "@openapitools/openapi-generator-cli": "^2.5.2", 36 | "adm-zip": "^0.5.10", 37 | "ansi-colors": "^4.1.1", 38 | "axios": "1.7.7", 39 | "moment": "^2.29.4", 40 | "word-wrap": "^1.2.5" 41 | }, 42 | "devDependencies": { 43 | "@types/jest": "^29.2.1", 44 | "@types/node": "^20.11.16", 45 | "@typescript-eslint/eslint-plugin": "^6.21.0", 46 | "@vercel/ncc": "^0.38.1", 47 | "axios-mock-adapter": "^1.21.2", 48 | "eslint": "^8.39.0", 49 | "eslint-plugin-github": "^4.3.2", 50 | "eslint-plugin-jest": "^27.6.3", 51 | "jest": "^29.2.2", 52 | "jest-circus": "^29.2.2", 53 | "ts-jest": "^29.1.1", 54 | "typescript": "^4.5.4", 55 | "validator": "^13.7.0" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/action.ts: -------------------------------------------------------------------------------- 1 | import * as artifact from "@actions/artifact" 2 | import * as core from "@actions/core" 3 | import * as path from "path" 4 | import ConfigurationFactory, { Config } from "./config" 5 | import { ExecutionGraph, ExecutionGraphReport, Pipeline, SemanticValidationHint, SemanticValidationLevel, Task, 6 | TaskStatus } from "./client/vib/api" 7 | import { BASE_PATH } from "./client/vib/base" 8 | import fs from "fs" 9 | import CSP from "./client/csp" 10 | import VIB from "./client/vib" 11 | import ansi from "ansi-colors" 12 | import moment from "moment" 13 | import { pipeline as streamPipeline } from "node:stream/promises" 14 | import AdmZip from "adm-zip" 15 | import { Readable } from "stream" 16 | import { randomUUID } from "crypto" 17 | 18 | export interface ActionResult { 19 | baseDir: string, 20 | artifacts: string[], 21 | executionGraph: ExecutionGraph, 22 | executionGraphReport: ExecutionGraphReport | undefined 23 | } 24 | 25 | class Action { 26 | 27 | private ENV_VAR_TEMPLATE_PREFIX = "VIB_ENV_" 28 | 29 | config: Config 30 | 31 | csp: CSP 32 | 33 | vib: VIB 34 | 35 | root: string 36 | 37 | constructor(root: string) { 38 | this.config = new ConfigurationFactory(root).getConfiguration() 39 | this.root = root 40 | this.csp = new CSP(this.config.clientTimeoutMillis, this.config.clientRetryCount, this.config.clientRetryIntervals) 41 | this.vib = new VIB(this.config.clientTimeoutMillis, this.config.clientRetryCount, this.config.clientRetryIntervals, 42 | this.config.clientUserAgentVersion, this.csp) 43 | } 44 | 45 | async main(): Promise { 46 | core.startGroup("Initializing GitHub Action...") 47 | const pipeline = await this.initialize() 48 | core.endGroup() 49 | 50 | core.startGroup("Running pipeline...") 51 | const executionGraph = await this.runPipeline(pipeline) 52 | core.endGroup() 53 | 54 | core.startGroup("Processing resulting execution graph...") 55 | const actionResult = await this.processExecutionGraph(executionGraph) 56 | core.endGroup() 57 | 58 | core.startGroup("Uploading artifacts...") 59 | await this.uploadArtifacts(actionResult.baseDir, actionResult.artifacts, executionGraph.execution_graph_id) 60 | core.endGroup() 61 | await this.rmdir(actionResult.baseDir) 62 | 63 | this.summarize(executionGraph, actionResult) 64 | 65 | return actionResult 66 | } 67 | 68 | async initialize(): Promise { 69 | await this.checkCSPTokenExpiration() 70 | return await this.readPipeline() 71 | } 72 | 73 | async checkCSPTokenExpiration(): Promise { 74 | core.debug(`Checking CSP token expiration, token expiration days warning set to ${this.config.tokenExpirationDaysWarning}`) 75 | const tokenExpiration = await this.csp.checkTokenExpiration() 76 | const now = moment() 77 | const expiresAt = moment.unix(tokenExpiration) 78 | const expiresInDays = expiresAt.diff(now, "days") 79 | if (expiresInDays < this.config.tokenExpirationDaysWarning) { 80 | core.warning(`CSP API token will expire in ${expiresInDays} days.`) 81 | } else { 82 | core.debug(`Checked expiration token, expires ${expiresAt.from(now)}.`) 83 | } 84 | } 85 | 86 | async readPipeline(): Promise { 87 | core.debug(`Reading pipeline from ${this.root}, using base folder ${this.config.baseFolder} and file ${this.config.pipeline}`) 88 | let rawPipeline = fs.readFileSync(path.join(this.root, this.config.baseFolder, this.config.pipeline)).toString() 89 | 90 | if (this.config.shaArchive) { 91 | rawPipeline = rawPipeline.replace(/{SHA_ARCHIVE}/g, this.config.shaArchive) 92 | } else if (rawPipeline.includes("{SHA_ARCHIVE}")) { 93 | throw new Error(`Pipeline ${this.config.pipeline} expects SHA_ARCHIVE variable but either GITHUB_REPOSITORY or GITHUB_SHA cannot be found on environment.`) 94 | } 95 | 96 | if (this.config.targetPlatform) { 97 | rawPipeline = rawPipeline.replace(/{TARGET_PLATFORM}/g, this.config.targetPlatform) 98 | } 99 | 100 | for (const key of Object.keys(process.env).filter(k => k.startsWith(this.ENV_VAR_TEMPLATE_PREFIX))) { 101 | const value = process.env[key] 102 | rawPipeline = this.replaceVariable(rawPipeline, key, value === undefined ? '' : value) 103 | } 104 | 105 | const unsubstituted = [...rawPipeline.matchAll(/((? { 157 | const startTime = Date.now() 158 | 159 | const validationHints = await this.vib.validatePipeline(pipeline) 160 | this.displayPipelineValidationHints(validationHints) 161 | 162 | core.info(ansi.bold(ansi.green("The pipeline has been validated successfully."))) 163 | 164 | const executionGraphId = await this.vib.createPipeline(pipeline, this.config.pipelineDurationMillis, this.config.verificationMode) 165 | core.info(`Running execution graph: ${BASE_PATH}/execution-graphs/${executionGraphId}`) 166 | 167 | const executionGraph = await new Promise((resolve, reject) => { 168 | 169 | const unconcludedTasks: Task[] = [] 170 | 171 | const interval = setInterval(async () => { 172 | 173 | try { 174 | const eg = await this.vib.getExecutionGraph(executionGraphId) 175 | const status = eg.status 176 | 177 | // eslint-disable-next-line max-len 178 | unconcludedTasks.push(...this.displayUnconcludedTasks(eg, eg.tasks.filter(t => !unconcludedTasks.find(f => f.task_id === t.task_id)))) 179 | 180 | if (status === TaskStatus.Failed || status === TaskStatus.Skipped || status === TaskStatus.Succeeded) { 181 | resolve(eg) 182 | clearInterval(interval) 183 | } else if (Date.now() - startTime > this.config.pipelineDurationMillis) { 184 | throw new Error(`Pipeline ${executionGraphId} timed out. Ending pipeline execution.`) 185 | } else { 186 | core.info(`Execution graph ${executionGraphId} in progress, will check in ${this.config.executionGraphCheckInterval / 1000}s.`) 187 | } 188 | } catch(err) { 189 | clearInterval(interval) 190 | reject(err) 191 | } 192 | }, this.config.executionGraphCheckInterval) 193 | }) 194 | 195 | core.setOutput("execution-graph", executionGraph) 196 | 197 | return executionGraph 198 | } 199 | 200 | private displayPipelineValidationHints(hints: SemanticValidationHint[]): void { 201 | const header = 'Got pipeline validation hint: ' 202 | for (const hint of hints) { 203 | const message = header + hint.message 204 | switch (hint.level) { 205 | case SemanticValidationLevel.Error: 206 | core.error(message) 207 | break 208 | case SemanticValidationLevel.Warning: 209 | core.warning(message) 210 | break 211 | case SemanticValidationLevel.Info: 212 | default: 213 | core.info(message) 214 | break 215 | } 216 | } 217 | } 218 | 219 | private displayUnconcludedTasks(executionGraph: ExecutionGraph, tasks: Task[]): Task[] { 220 | const unconcluded: Task[] = [] 221 | for (const task of tasks.filter(t => t.status === TaskStatus.Failed || t.status === TaskStatus.Skipped)) { 222 | let name = task.action_id 223 | 224 | if (name === "deployment") { 225 | name = name.concat(` (${executionGraph.tasks.find(t => t.task_id === task.next_tasks[0])?.action_id})`) 226 | } else if (name === "undeployment") { 227 | name = name.concat(` (${executionGraph.tasks.find(t => t.task_id === task.previous_tasks[0])?.action_id})`) 228 | } 229 | 230 | if (task.status === TaskStatus.Failed) { 231 | core.error(`Task ${name} with ID ${task.task_id} has failed. Error: ${task.error}`) 232 | } else if (task.status === TaskStatus.Skipped) { 233 | core.error(`Task ${name} with ID ${task.task_id} was skipped. Error: ${task.error}`) 234 | } 235 | unconcluded.push(task) 236 | } 237 | return unconcluded 238 | } 239 | 240 | async processExecutionGraph(executionGraph: ExecutionGraph): Promise { 241 | const executionGraphId = executionGraph.execution_graph_id 242 | const artifacts: string[] = [] 243 | 244 | const outputsDir = path.join(this.root, "outputs", randomUUID()) 245 | const bundleDir = this.mkdir(path.join(outputsDir, executionGraphId)) 246 | 247 | let executionGraphReport: ExecutionGraphReport | undefined = undefined 248 | 249 | try { 250 | const executionGraphBundle: Readable = await this.downloadBundle(executionGraphId) 251 | const bundleFiles: string[] = await this.extractZip(executionGraphBundle, outputsDir) 252 | artifacts.push(...bundleFiles) 253 | 254 | executionGraphReport = JSON.parse(fs.readFileSync(path.join(bundleDir, 'report.json')).toString()) 255 | } catch (error) { 256 | core.warning(`Error downloading bundle files for execution graph ${executionGraphId}, error: ${error}`) 257 | } 258 | 259 | if (executionGraph.status === TaskStatus.Succeeded && !executionGraphReport?.passed) { 260 | core.setFailed("Execution graph succeeded, however some tasks didn't pass the verification.") 261 | } else if (executionGraph.status !== TaskStatus.Succeeded) { 262 | core.setFailed(`Execution graph ${executionGraphId} has ${executionGraph.status.toLowerCase()}.`) 263 | } 264 | 265 | return { baseDir: bundleDir, artifacts, executionGraph, executionGraphReport } 266 | } 267 | 268 | private async downloadBundle(executionGraphId: string): Promise { 269 | let retries = 2 270 | const waitIntervalMillis = 2 * 1000 271 | 272 | const bundle: Readable = await new Promise((resolve, reject) => { 273 | const interval = setInterval(async () => { 274 | try { 275 | const result = await this.vib.getExecutionGraphBundle(executionGraphId) 276 | resolve(result) 277 | clearInterval(interval) 278 | } catch(err) { 279 | if (retries === 0) { 280 | reject(err) 281 | clearInterval(interval) 282 | } else { 283 | core.warning(`Download of the execution graph bundle failed, there are ${retries} retries left.`) 284 | retries-- 285 | } 286 | } 287 | }, waitIntervalMillis) 288 | }) 289 | 290 | return bundle 291 | } 292 | 293 | private async extractZip(from: Readable, basePath: string): Promise { 294 | const tmp = path.join(basePath, 'bundle.zip') 295 | const artifacts: string[] = [] 296 | await streamPipeline(from, fs.createWriteStream(tmp)) 297 | const zip = new AdmZip(tmp) 298 | for (const zipEntry of zip.getEntries()) { 299 | if (!zipEntry.isDirectory && !zipEntry.entryName.startsWith('__MACOSX')) { 300 | artifacts.push(path.join(basePath, zipEntry.entryName)) 301 | } 302 | } 303 | zip.extractAllTo(basePath) 304 | return artifacts 305 | } 306 | 307 | private mkdir(dir: string): string { 308 | core.debug(`Creating directory ${dir} if does not exist`) 309 | if (!fs.existsSync(dir)) { 310 | fs.mkdirSync(dir, { recursive: true }) 311 | } 312 | return dir 313 | } 314 | 315 | private async rmdir(outputsDir: string): Promise { 316 | core.debug(`Removing directory ${outputsDir} after action finishes.`) 317 | if (fs.existsSync(outputsDir)) { 318 | try { 319 | await fs.promises.rm(outputsDir, { recursive: true }) 320 | } catch (error) { 321 | core.warning(`Error removing directory ${outputsDir}. Error: ${error}`) 322 | } 323 | } 324 | return outputsDir 325 | } 326 | 327 | createArtifactClient(): artifact.ArtifactClient { 328 | return new artifact.DefaultArtifactClient() 329 | } 330 | 331 | async uploadArtifacts(baseDir: string, artifacts: string[], executionGraphId: string): Promise { 332 | if (process.env.ACTIONS_RUNTIME_TOKEN && this.config.uploadArtifacts && artifacts.length > 0) { 333 | const artifactClient = this.createArtifactClient() 334 | const artifactName = await this.getArtifactName(executionGraphId) 335 | 336 | try { 337 | await artifactClient.uploadArtifact(artifactName, artifacts, baseDir) 338 | } catch (error) { 339 | core.warning(`Unexpected error uploading the artifacts ${this.config.targetPlatform}, error: ${error}`) 340 | } 341 | } else if (!this.config.uploadArtifacts) { 342 | core.info("Artifacts will not be published.") 343 | } else if (artifacts.length > 0) { 344 | core.warning("ACTIONS_RUNTIME_TOKEN env variable not found. Skipping upload artifacts.") 345 | } 346 | } 347 | 348 | private async getArtifactName(executionGraphID: string): Promise { 349 | core.debug('Generating artifact name') 350 | let artifactName = `assets-${process.env.GITHUB_JOB}` 351 | 352 | if (this.config.targetPlatform) { 353 | try { 354 | const targetPlatform = await this.vib.getTargetPlatform(this.config.targetPlatform) 355 | if (targetPlatform) { 356 | artifactName += `-${targetPlatform.kind}` 357 | } 358 | } catch (error) { 359 | core.warning(`Unexpected error getting target platform ${this.config.targetPlatform}, error: ${error}`) 360 | } 361 | } 362 | 363 | if (process.env.GITHUB_RUN_ATTEMPT) { 364 | const runAttempt = parseInt(process.env.GITHUB_RUN_ATTEMPT) 365 | if (runAttempt > 1) { 366 | artifactName += `_${runAttempt}` 367 | } 368 | } 369 | 370 | if (executionGraphID) { 371 | artifactName += `-${executionGraphID.slice(0, 8)}` 372 | } 373 | 374 | return artifactName 375 | } 376 | 377 | summarize(executionGraph: ExecutionGraph, actionResult: ActionResult): void { 378 | this.prettifyExecutionGraphResult(executionGraph, actionResult.executionGraphReport) 379 | // TODO: add cleanup function to remove local artifacts 380 | } 381 | 382 | prettifyExecutionGraphResult(executionGraph: ExecutionGraph, report?: ExecutionGraphReport): void { 383 | if (!report) { 384 | return core.warning('Skipping execution graph summary, either the report could not be dowloaded or final state was not SUCCEEDED') 385 | } 386 | 387 | core.info(ansi.bold(`Pipeline result: ${report.passed ? ansi.green("passed") : ansi.red("failed")}`)) 388 | core.summary.addHeading(`Pipeline result: ${report.passed ? "passed" : "failed"}`) 389 | 390 | let tasksPassed = 0 391 | let tasksFailed = 0 392 | 393 | let testsTable = "" 394 | + "" 395 | let vulnerabilitiesTable = "
Tests
ActionArchitecturePassed 🟢SkippedFailed 🔴Result
" 396 | + "" 397 | + "" 398 | const infoMessage = "ℹ️ By policy the pipeline does not block releases with non fixed" 399 | + " vulnerabilities in thirdparty components." 400 | 401 | for (const task of report.actions) { 402 | if (task.passed !== undefined && task.passed !== null) { 403 | if (task.passed === true) { 404 | tasksPassed++ 405 | } else { 406 | tasksFailed++ 407 | } 408 | } 409 | 410 | if (task.tests) { 411 | core.info(`${ansi.bold(`${task.action_id} action:`)} ${task.passed === true ? ansi.green("passed") : ansi.red("failed")} » ` 412 | + `${"Tests:"} ${ansi.bold(ansi.green(`${task.tests.passed} passed`))}, ` 413 | + `${ansi.bold(ansi.yellow(`${task.tests.skipped} skipped`))}, ` 414 | + `${ansi.bold(ansi.red(`${task.tests.failed} failed`))}`) 415 | testsTable += this.testTableRow(task.action_id, task.architecture || 'amd64', task.tests.passed, 416 | task.tests.skipped, task.tests.failed, task.passed) 417 | } else if (task.vulnerabilities) { 418 | core.info(`${ansi.bold(`${task.action_id} action:`)} ${task.passed === true ? ansi.green("passed") : ansi.red("failed")} » ` 419 | + `${"Vulnerabilities:"} ${task.vulnerabilities.minimal} minimal, ` 420 | + `${task.vulnerabilities.low} low, ` 421 | + `${task.vulnerabilities.medium} medium, ` 422 | + `${task.vulnerabilities.high} high, ` 423 | + `${ansi.bold(`${task.vulnerabilities.critical} critical`)}, ` 424 | + `${task["vulnerabilities"]["unknown"]} unknown`) 425 | vulnerabilitiesTable += this.vulnerabilitiesTableRow(task.action_id, task.architecture || 'amd64', 426 | task.vulnerabilities.minimal, task.vulnerabilities.low, 427 | task.vulnerabilities.medium, task.vulnerabilities.high, task.vulnerabilities.critical, task.vulnerabilities.unknown, task.passed) 428 | } 429 | } 430 | 431 | vulnerabilitiesTable += `` 432 | 433 | const tasksSkipped = executionGraph.tasks.filter(t => t.status === TaskStatus.Skipped).length 434 | 435 | core.info(ansi.bold(`Actions: ` 436 | + `${ansi.green(`${tasksPassed} passed`)}, ` 437 | + `${ansi.yellow(`${tasksSkipped} skipped`)}, ` 438 | + `${ansi.red(`${tasksFailed} failed`)}, ` 439 | + `${tasksPassed + tasksFailed + tasksSkipped} total`) 440 | ) 441 | 442 | const testsTableRows = testsTable.split("").length -1 443 | if (testsTableRows > 2) { 444 | core.summary.addRaw(testsTable) 445 | } 446 | 447 | const vulnerabilitiesTableRows = vulnerabilitiesTable.split("").length -1 448 | if (vulnerabilitiesTableRows > 2) { 449 | core.summary.addRaw(vulnerabilitiesTable) 450 | } 451 | 452 | if (process.env.GITHUB_STEP_SUMMARY) core.summary.write() 453 | } 454 | 455 | private testTableRow(action: string, architecture: string | undefined, passed: number, skipped: number, 456 | failed: number, actionPassed: boolean | undefined): string { 457 | return `` 458 | } 459 | 460 | private vulnerabilitiesTableRow(action: string, architecture: string | undefined, min: number, low: number, 461 | mid: number, high: number, critic: number, unk: number, 462 | passed: boolean | undefined): string { 463 | return `` 464 | } 465 | } 466 | 467 | export default Action 468 | -------------------------------------------------------------------------------- /src/client/clients.ts: -------------------------------------------------------------------------------- 1 | import * as core from "@actions/core" 2 | import type {AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig} from "axios" 3 | import axios, { AxiosError, AxiosHeaders} from "axios" 4 | 5 | const RETRIABLE_ERROR_CODES = ["ECONNABORTED", "ECONNREFUSED"] 6 | 7 | enum RetriableHttpStatus { 8 | BAD_GATEWAY = 502, 9 | SERVICE_NOT_AVAILABLE = 503, 10 | REQUEST_TIMEOUT = 408, 11 | TOO_MANY_REQUESTS = 429, 12 | } 13 | 14 | const SLOW_REQUEST_THRESHOLD = 30000 15 | 16 | export interface ClientConfig { 17 | retries: number, 18 | backoffIntervals: number[], 19 | retriableErrorCodes?: string[], 20 | } 21 | 22 | export function newClient(axiosCfg: AxiosRequestConfig, clientCfg: ClientConfig): AxiosInstance { 23 | const instance = axios.create(axiosCfg) 24 | 25 | instance.interceptors.request.use(async (config: InternalAxiosRequestConfig) => { 26 | config["startTime"] = new Date() 27 | return config 28 | }) 29 | 30 | instance.interceptors.response.use( 31 | async (response: AxiosResponse) => { 32 | if (response && response.config) { 33 | const endTime = new Date() 34 | const duration = endTime.getTime() - response.config["startTime"].getTime() 35 | if (duration > SLOW_REQUEST_THRESHOLD) { 36 | core.info(`Slow response detected: ${duration}ms`) 37 | } 38 | } 39 | return response 40 | }, 41 | async (err: AxiosError) => { 42 | const config = err.config 43 | const response = err.response 44 | const maxRetries = clientCfg.retries 45 | const backoffIntervals = clientCfg.backoffIntervals 46 | const retriableErrorCodes = clientCfg.retriableErrorCodes ? clientCfg.retriableErrorCodes : RETRIABLE_ERROR_CODES 47 | 48 | core.debug( 49 | `Error: ${JSON.stringify(err)}. Status: ${response ? response.status : "unknown"}. Data: ${ 50 | response ? JSON.stringify(response.data) : "unknown" 51 | }` 52 | ) 53 | 54 | if ( 55 | response && response.status && Object.values(RetriableHttpStatus).includes(response.status) || 56 | err.code !== undefined && retriableErrorCodes.includes(err.code) || 57 | err.message === "Network Error" 58 | ) { 59 | // Not sure if this message is trustable or just something moxios made up 60 | if (config == null) { 61 | core.debug("Could not find configuration on axios error. Exiting.") 62 | return Promise.reject(err) 63 | } 64 | 65 | //TODO: To be removed when https://github.com/axios/axios/issues/5089 gets closed. 66 | config.headers = config.headers || new AxiosHeaders() 67 | 68 | const currentState = config["vib-retries"] || {} 69 | currentState.retryCount = currentState.retryCount || 0 70 | config["vib-retries"] = currentState 71 | 72 | const index = 73 | currentState.retryCount >= backoffIntervals.length ? backoffIntervals.length - 1 : currentState.retryCount 74 | let delay = backoffIntervals[index] 75 | 76 | if (response && response.headers && response.headers["Retry-After"]) { 77 | const retryAfter = Number.parseInt(response.headers["Retry-After"]) 78 | if (!Number.isNaN(retryAfter)) { 79 | delay = Number.parseInt(response.headers["Retry-After"]) * 1000 80 | core.debug(`Following server advice. Will retry after ${response.headers["Retry-After"]} seconds`) 81 | } else { 82 | core.debug(`Could not parse Retry-After header value ${response.headers["Retry-After"]}`) 83 | } 84 | } 85 | 86 | if (currentState.retryCount >= maxRetries) { 87 | core.debug("The number of retries exceeds the limit.") 88 | return Promise.reject(new Error(`Could not execute operation. Retried ${currentState.retryCount} times.`)) 89 | } else { 90 | core.info( 91 | `Request to ${config.url} failed. Retry: ${currentState.retryCount}. Waiting ${delay}. [Error: ${ 92 | err.message 93 | }, Status: ${response ? response.status : "unknown"}, Response headers: ${JSON.stringify( 94 | response?.headers 95 | )}` 96 | ) 97 | currentState.retryCount += 1 98 | } 99 | config.transformRequest = [data => data] 100 | 101 | return new Promise(resolve => 102 | setTimeout(() => { 103 | config["startTime"] = new Date() // Reset slow response count 104 | resolve(instance(config)) 105 | }, delay) 106 | ) 107 | } else { 108 | core.debug( 109 | `Error message: ${err.message}. Status: ${response ? response.status : "unknown"}. 110 | Response headers: ${response?.headers}. Stack: ${err.stack}` 111 | ) 112 | 113 | return Promise.reject(err) 114 | } 115 | } 116 | ) 117 | 118 | return instance 119 | } 120 | -------------------------------------------------------------------------------- /src/client/csp.ts: -------------------------------------------------------------------------------- 1 | import * as core from "@actions/core" 2 | import type { AxiosInstance } from "axios" 3 | import {isAxiosError} from "axios" 4 | import { newClient } from "./clients" 5 | import util from "util" 6 | 7 | const DEFAULT_CSP_API_URL = "https://console.tanzu.broadcom.com" 8 | 9 | const TOKEN_DETAILS_PATH = "/csp/gateway/am/api/auth/api-tokens/details" 10 | 11 | const TOKEN_AUTHORIZE_PATH = "/csp/gateway/am/api/auth/api-tokens/authorize" 12 | 13 | const TOKEN_TIMEOUT_MILLIS = 10 * 60 * 1000 // 10 minutes 14 | 15 | interface CspToken { 16 | access_token: string 17 | timestamp: number 18 | } 19 | 20 | class CSP { 21 | client: AxiosInstance 22 | cachedCspToken: CspToken | null = null 23 | 24 | constructor(clientTimeout: number, clientRetryCount: number, clientRetryIntervals: number[]) { 25 | this.client = newClient( 26 | { 27 | baseURL: process.env.CSP_API_URL ? process.env.CSP_API_URL : DEFAULT_CSP_API_URL, 28 | timeout: clientTimeout, 29 | headers: { 30 | "Content-Type": "application/x-www-form-urlencoded", 31 | }, 32 | }, 33 | { 34 | retries: clientRetryCount, 35 | backoffIntervals: clientRetryIntervals, 36 | } 37 | ) 38 | } 39 | 40 | async checkTokenExpiration(): Promise { 41 | if (!process.env.CSP_API_TOKEN) { 42 | throw new Error("CSP_API_TOKEN secret not found.") 43 | } 44 | 45 | const response = await this.client.post( 46 | TOKEN_DETAILS_PATH, 47 | { tokenValue: process.env.CSP_API_TOKEN }, 48 | { 49 | headers: { 50 | "Content-Type": "application/json", 51 | }, 52 | } 53 | ) 54 | 55 | return response.data.expiresAt 56 | } 57 | 58 | async getToken(timeout?: number): Promise { 59 | if (!process.env.CSP_API_TOKEN) { 60 | throw new Error("CSP_API_TOKEN secret not found.") 61 | } 62 | 63 | if (this.cachedCspToken != null && this.cachedCspToken.timestamp > Date.now()) { 64 | return this.cachedCspToken.access_token 65 | } 66 | 67 | try { 68 | const response = await this.client.post( 69 | TOKEN_AUTHORIZE_PATH, 70 | `grant_type=refresh_token&api_token=${process.env.CSP_API_TOKEN}` 71 | ) 72 | 73 | //TODO: Handle response codes 74 | core.debug(`Got response from CSP API token ${util.inspect(response.data)}`) 75 | if (!response.data || !response.data.access_token) { 76 | throw new Error("Could not fetch access token, got empty response from CSP.") 77 | } 78 | 79 | this.setCachedToken({ 80 | access_token: response.data.access_token, 81 | timestamp: Date.now() + (timeout || TOKEN_TIMEOUT_MILLIS), 82 | }) 83 | 84 | core.debug("CSP API token obtained successfully.") 85 | return response.data.access_token 86 | } catch (error) { 87 | if (isAxiosError(error) && error.response) { 88 | if (error.response.status === 404 || error.response.status === 400) { 89 | core.debug(util.inspect(error.response.data)) 90 | throw new Error(`Could not obtain CSP API token. Status code: ${error.response.status}.`) 91 | } 92 | } 93 | throw error 94 | } 95 | } 96 | 97 | setCachedToken(token: CspToken | null): void { 98 | this.cachedCspToken = token 99 | } 100 | } 101 | 102 | export default CSP 103 | -------------------------------------------------------------------------------- /src/client/vib.ts: -------------------------------------------------------------------------------- 1 | import * as core from "@actions/core" 2 | import type { AxiosInstance, InternalAxiosRequestConfig } from "axios" 3 | import { ConstraintsViolation, ExecutionGraph, ExecutionGraphsApi, Pipeline, PipelinesApi, RawReport, 4 | SemanticValidationHint, 5 | TargetPlatform, TargetPlatformsApi } from "./vib/api" 6 | import CSP from "./csp" 7 | import { Readable } from "stream" 8 | import { AxiosHeaders, isAxiosError } from "axios" 9 | import moment from "moment" 10 | import { newClient } from "./clients" 11 | import util from "util" 12 | 13 | export enum VerificationModes { 14 | PARALLEL = "PARALLEL", 15 | SERIAL = "SERIAL", 16 | } 17 | 18 | const DEFAULT_VERIFICATION_MODE = VerificationModes.PARALLEL 19 | 20 | class VIB { 21 | executionGraphsClient: ExecutionGraphsApi 22 | pipelinesClient: PipelinesApi 23 | targetPlatformsClient: TargetPlatformsApi 24 | 25 | constructor(clientTimeout: number, clientRetryCount: number, clientRetryIntervals: number[], clientUserAgent: string, 26 | csp?: CSP) { 27 | const client: AxiosInstance = newClient( 28 | { 29 | timeout: clientTimeout, 30 | headers: { 31 | "Content-Type": "application/json", 32 | "User-Agent": `vib-action/${clientUserAgent}`, 33 | }, 34 | }, 35 | { 36 | retries: clientRetryCount, 37 | backoffIntervals: clientRetryIntervals, 38 | } 39 | ) 40 | 41 | if (csp) { 42 | client.interceptors.request.use(async (config: InternalAxiosRequestConfig) => { 43 | if (!config.headers) { 44 | config.headers = new AxiosHeaders() 45 | } 46 | 47 | config.headers.set("Authorization", `Bearer ${await csp.getToken()}`) 48 | return config 49 | }) 50 | } 51 | 52 | 53 | this.executionGraphsClient = new ExecutionGraphsApi(undefined, undefined, client) 54 | this.pipelinesClient = new PipelinesApi(undefined, undefined, client) 55 | this.targetPlatformsClient = new TargetPlatformsApi(undefined, undefined, client) 56 | } 57 | 58 | async createPipeline( 59 | pipeline: Pipeline, 60 | pipelineDurationMillis: number, 61 | verificationMode?: VerificationModes 62 | ): Promise { 63 | try { 64 | core.debug(`Sending pipeline [pipeline=${util.inspect(pipeline)}]`) 65 | 66 | const response = await this.pipelinesClient.startPipeline(pipeline, undefined, { 67 | headers: { 68 | "X-Verification-Mode": `${verificationMode || DEFAULT_VERIFICATION_MODE}`, 69 | "X-Expires-After": moment() 70 | .add(pipelineDurationMillis / 1000.0, "s") 71 | .format("ddd, DD MMM YYYY HH:mm:ss z"), 72 | } 73 | }) 74 | 75 | core.debug(`Got response.data : ${JSON.stringify(response.data)}, headers: ${util.inspect(response.headers)}`) 76 | 77 | //TODO: Handle response codes 78 | const locationHeader: string = response.headers["location"]?.toString() 79 | if (!locationHeader) { 80 | throw new Error("Location header not found") 81 | } 82 | 83 | return locationHeader.substring(locationHeader.lastIndexOf("/") + 1) 84 | } catch (error) { 85 | core.debug(JSON.stringify(error)) 86 | throw new Error(`Unexpected error creating pipeline.`) 87 | } 88 | } 89 | 90 | async getExecutionGraph(executionGraphId: string): Promise { 91 | try { 92 | core.debug(`Getting execution graph [id=${executionGraphId}]`) 93 | 94 | const response = await this.executionGraphsClient.getExecutionGraph(executionGraphId) 95 | 96 | core.debug(`Got response.data : ${JSON.stringify(response.data)}, headers: ${util.inspect(response.headers)}`) 97 | 98 | //TODO: Handle response codes 99 | return response.data 100 | } catch (err) { 101 | if (isAxiosError(err) && err.response) { 102 | core.debug(JSON.stringify(err.toJSON())) 103 | if (err.response.status === 404) { 104 | throw new Error( 105 | err.response.data ? err.response.data.detail : `Could not find execution graph with id ${executionGraphId}` 106 | ) 107 | } 108 | throw err 109 | } 110 | throw err 111 | } 112 | } 113 | 114 | async getRawLogs(executionGraphId: string, taskId: string): Promise { 115 | try { 116 | core.debug(`Downloading raw logs [executionGraphId=${executionGraphId}, taskId=${taskId}]`) 117 | 118 | const response = await this.executionGraphsClient.getRawTaskLogs(executionGraphId, taskId) 119 | 120 | core.debug(`Got response.data : ${JSON.stringify(response.data)}, headers: ${util.inspect(response.headers)}`) 121 | 122 | //TODO: Handle response codes 123 | return response.data 124 | } catch (err) { 125 | if (isAxiosError(err) && err.response) { 126 | core.debug(JSON.stringify(err.toJSON())) 127 | throw new Error( 128 | `Error fetching logs for task ${taskId}. Code: ${err.response.status}. Message: ${err.response.statusText}` 129 | ) 130 | } else { 131 | throw err 132 | } 133 | } 134 | } 135 | 136 | async getRawReport(executionGraphId: string, taskId: string, reportId: string): Promise { 137 | try { 138 | core.debug(`Downloading raw report [executionGraphId=${executionGraphId}, taskId=${taskId}, reportId=${reportId}]`) 139 | 140 | const response = await this.executionGraphsClient.getTaskResultRawReportById(executionGraphId, taskId, reportId, { 141 | responseType: "stream", 142 | }) 143 | 144 | //TODO: Handle response codes 145 | return response.data as unknown as Readable // Hack bc the autogenerated client says it's a string 146 | } catch (err) { 147 | if (isAxiosError(err) && err.response) { 148 | core.debug(JSON.stringify(err.toJSON())) 149 | throw new Error( 150 | `Error fetching raw report ${reportId} for task ${taskId}. Code: ${err.response.status}. Message: ${err.response.statusText}` 151 | ) 152 | } else { 153 | throw err 154 | } 155 | } 156 | } 157 | 158 | async getRawReports(executionGraphId: string, taskId: string): Promise { 159 | try { 160 | core.debug(`Getting raw reports [executionGraphId=${executionGraphId}, taskId=${taskId}]`) 161 | 162 | const response = await this.executionGraphsClient.getTaskResultRawReports(executionGraphId, taskId) 163 | 164 | //TODO: Handle response codes 165 | return response.data 166 | } catch (err) { 167 | if (isAxiosError(err) && err.response) { 168 | core.debug(JSON.stringify(err.toJSON())) 169 | throw new Error( 170 | `Error fetching raw reports for task ${taskId}. Code: ${err.response.status}. Message: ${err.response.statusText}` 171 | ) 172 | } else { 173 | throw err 174 | } 175 | } 176 | } 177 | 178 | async getExecutionGraphBundle(executionGraphId: string): Promise { 179 | try { 180 | core.debug(`Getting bundle [executionGraphId=${executionGraphId}`) 181 | 182 | const response = await this.executionGraphsClient.getExecutionGraphBundle(executionGraphId, { 183 | responseType: "stream", 184 | }) 185 | 186 | //TODO: Handle response codes 187 | return response.data as unknown as Readable // Hack bc the autogenerated client says it's a string 188 | } catch (err) { 189 | if (isAxiosError(err) && err.response) { 190 | core.debug(JSON.stringify(err.toJSON())) 191 | throw new Error( 192 | `Error fetching bundle for execution graph ${executionGraphId}. Code: ${err.response.status}. Message: ${err.response.statusText}` 193 | ) 194 | } else { 195 | throw err 196 | } 197 | } 198 | } 199 | 200 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 201 | async getTaskReport(executionGraphId: string, taskId: string): Promise<{[key: string]: any}> { 202 | try { 203 | core.debug(`Downloading task report [executionGraphId=${executionGraphId}, taskId=${taskId}]`) 204 | 205 | const response = await this.executionGraphsClient.getTaskResultReport(executionGraphId, taskId) 206 | 207 | core.debug(`Got response.data : ${JSON.stringify(response.data)}, headers: ${util.inspect(response.headers)}`) 208 | 209 | //TODO: Handle response codes 210 | return response.data 211 | } catch (err) { 212 | if (isAxiosError(err) && err.response) { 213 | core.debug(JSON.stringify(err.toJSON())) 214 | throw new Error( 215 | `Error fetching task ${taskId} report. Code: ${err.response.status}. Message: ${err.response.statusText}` 216 | ) 217 | } else { 218 | throw err 219 | } 220 | } 221 | } 222 | 223 | async getTargetPlatform(targetPlatformId: string): Promise { 224 | try { 225 | core.debug(`Getting target platform ${targetPlatformId}`) 226 | 227 | const response = await this.targetPlatformsClient.getTargetPlatform(targetPlatformId) 228 | 229 | //TODO: Handle response codes 230 | return response.data 231 | } catch (err) { 232 | if (isAxiosError(err) && err.response) { 233 | core.debug(JSON.stringify(err.toJSON())) 234 | throw new Error( 235 | `Error fetching target platform. Code: ${err.response.status}. Message: ${err.response.statusText}` 236 | ) 237 | } else { 238 | throw err 239 | } 240 | } 241 | } 242 | 243 | async getTargetPlatforms(): Promise { 244 | try { 245 | core.debug(`Getting target platforms`) 246 | 247 | const response = await this.targetPlatformsClient.getTargetPlatforms(undefined, undefined, undefined, undefined, 248 | undefined) 249 | 250 | //TODO: Handle response codes 251 | return response.data 252 | } catch (err) { 253 | if (isAxiosError(err) && err.response) { 254 | core.debug(JSON.stringify(err.toJSON())) 255 | throw new Error( 256 | `Error fetching target platforms. Code: ${err.response.status}. Message: ${err.response.statusText}` 257 | ) 258 | } else { 259 | throw err 260 | } 261 | } 262 | } 263 | 264 | async validatePipeline(pipeline: Pipeline): Promise { 265 | try { 266 | core.debug(`Validating pipeline [pipeline=${util.inspect(pipeline)}]`) 267 | 268 | const response = await this.pipelinesClient.validatePipeline(pipeline) 269 | 270 | core.debug(`Got response.data : ${JSON.stringify(response.data)}, headers: ${util.inspect(response.headers)}`) 271 | 272 | //TODO: Handle response codes 273 | return response.data 274 | } catch (error) { 275 | if (isAxiosError(error) && error.response) { 276 | if (error.response.status === 400) { 277 | throw new Error(error.response?.data?.violations.map((v: ConstraintsViolation) => `Field: ${v.field}. Error: ${v.message}`).toString()) 278 | } 279 | 280 | throw new Error( 281 | `Could not reach out to VIB. Please try again. Code: ${error.response.status}. Message: ${error.response.statusText}` 282 | ) 283 | } else { 284 | throw error 285 | } 286 | } 287 | } 288 | } 289 | 290 | export default VIB 291 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import * as core from "@actions/core" 2 | import * as path from "path" 3 | import { getNumberArray, getNumberInput } from "./util" 4 | import { VerificationModes } from "./client/vib" 5 | import fs from "fs" 6 | import util from "util" 7 | 8 | const DEFAULT_BASE_FOLDER = ".vib" 9 | 10 | const DEFAULT_EXECUTION_GRAPH_CHECK_INTERVAL_SECS = 30 // 30 seconds 11 | 12 | const DEFAULT_EXECUTION_GRAPH_GLOBAL_TIMEOUT_SECS = 90 * 60 // 90 minutes 13 | 14 | const DEFAULT_PIPELINE_FILE = "vib-pipeline.json" 15 | 16 | const DEFAULT_HTTP_TIMEOUT_MILLIS = 120000 17 | 18 | const DEFAULT_HTTP_RETRY_COUNT = 3 19 | 20 | const DEFAULT_HTTP_RETRY_INTERVALS_MILLIS = [5000, 10000, 15000] 21 | 22 | const MAX_GITHUB_ACTION_RUN_TIME_MILLIS = 360 * 60 * 1000 // 6 hours 23 | 24 | export interface Config { 25 | runtimeParametersFile: string, 26 | baseFolder: string, 27 | clientTimeoutMillis: number, 28 | clientRetryCount: number, 29 | clientRetryIntervals: number[], 30 | clientUserAgentVersion: string, 31 | configurationRoot: string, 32 | executionGraphCheckInterval: number, 33 | pipeline: string, 34 | pipelineDurationMillis: number, 35 | shaArchive: string | undefined, 36 | onlyUploadOnFailure: boolean, 37 | targetPlatform: string | undefined, 38 | tokenExpirationDaysWarning: number, 39 | uploadArtifacts: boolean, 40 | verificationMode: VerificationModes 41 | } 42 | 43 | class ConfigurationFactory { 44 | root: string 45 | 46 | constructor(root: string) { 47 | this.root = root 48 | } 49 | 50 | getConfiguration(): Config { 51 | const shaArchive = this.loadGitHubEvent() 52 | core.info(`Resources will be resolved from ${shaArchive}`) 53 | 54 | const baseFolder = core.getInput("config") || DEFAULT_BASE_FOLDER 55 | const pipeline = core.getInput("pipeline") || DEFAULT_PIPELINE_FILE 56 | 57 | const folderName = path.join(this.root, baseFolder) 58 | if (!fs.existsSync(folderName)) { 59 | core.setFailed(`Could not find base folder at ${folderName}`) 60 | } 61 | 62 | const filename = path.join(folderName, pipeline) 63 | if (!fs.existsSync(filename)) { 64 | core.setFailed(`Could not find pipeline at ${filename}`) 65 | } 66 | 67 | const rawVerificationMode = core.getInput("verification-mode") 68 | const verificationMode = VerificationModes[rawVerificationMode] 69 | if (!verificationMode) { 70 | core.warning( 71 | `The value ${rawVerificationMode} for verification-mode is not valid, the default value will be used.` 72 | ) 73 | } 74 | 75 | let pipelineDurationMillis = getNumberInput("max-pipeline-duration", DEFAULT_EXECUTION_GRAPH_GLOBAL_TIMEOUT_SECS) * 1000 76 | if (pipelineDurationMillis > MAX_GITHUB_ACTION_RUN_TIME_MILLIS) { 77 | pipelineDurationMillis = DEFAULT_EXECUTION_GRAPH_GLOBAL_TIMEOUT_SECS * 1000 78 | core.warning( 79 | `The value specified for the pipeline duration is larger than Github's allowed default. Pipeline will run with a duration of ${ 80 | pipelineDurationMillis / 1000 81 | } seconds.` 82 | ) 83 | } 84 | const runtimeParametersFile = core.getInput("runtime-parameters-file") 85 | 86 | const clientTimeoutMillis = getNumberInput("http-timeout", DEFAULT_HTTP_TIMEOUT_MILLIS) 87 | const clientRetryCount = getNumberInput("retry-count", DEFAULT_HTTP_RETRY_COUNT) 88 | const clientRetryIntervals = getNumberArray("backoff-intervals", DEFAULT_HTTP_RETRY_INTERVALS_MILLIS) 89 | const clientUserAgentVersion = process.env.GITHUB_ACTION_REF ? process.env.GITHUB_ACTION_REF : "unknown" 90 | 91 | const executionGraphCheckInterval = 92 | getNumberInput("execution-graph-check-interval", DEFAULT_EXECUTION_GRAPH_CHECK_INTERVAL_SECS) * 1000 93 | 94 | const config = { 95 | baseFolder, 96 | clientTimeoutMillis, 97 | clientRetryCount, 98 | clientRetryIntervals, 99 | clientUserAgentVersion, 100 | configurationRoot: this.root, 101 | executionGraphCheckInterval, 102 | runtimeParametersFile, 103 | pipeline, 104 | pipelineDurationMillis, 105 | shaArchive, 106 | onlyUploadOnFailure: core.getInput("only-upload-on-failure") === 'true', 107 | targetPlatform: process.env.VIB_ENV_TARGET_PLATFORM || process.env.TARGET_PLATFORM, 108 | tokenExpirationDaysWarning: 30, 109 | uploadArtifacts: core.getInput("upload-artifacts") === 'true', 110 | verificationMode, 111 | } 112 | 113 | core.debug(`Config: ${util.inspect(config)}`) 114 | 115 | return config 116 | } 117 | 118 | private loadGitHubEvent(): string | undefined { 119 | //TODO: Replace SHA_ARCHIVE with something more meaningful like PR_HEAD_TARBALL or some other syntax. 120 | // Perhaps something we could do would be to allow to use as variables to the actions any of the data 121 | // from the GitHub event from the GITHUB_EVENT_PATH file. For the time being I'm using pull_request.head.repo.url 122 | // plus the ref as the artifact name and reusing shaArchive but we need to redo this in the very short term 123 | const eventPath = process.env.GITHUB_EVENT_PATH_OVERRIDE ? process.env.GITHUB_EVENT_PATH_OVERRIDE 124 | : process.env.GITHUB_EVENT_PATH 125 | try { 126 | if (!eventPath) { 127 | throw new Error( 128 | "Could not find GITHUB_EVENT_PATH environment variable. Will not have any action event context." 129 | ) 130 | } 131 | 132 | core.info(`Loading event configuration from ${eventPath}`) 133 | 134 | const githubEvent = JSON.parse(fs.readFileSync(eventPath).toString()) 135 | core.debug(`Loaded config: ${util.inspect(githubEvent)}`) 136 | 137 | if (githubEvent["pull_request"]) { 138 | const headRef = `${githubEvent["pull_request"]["head"]["ref"]}` 139 | const encodedHeadRef = encodeURIComponent(headRef) 140 | return `${githubEvent["pull_request"]["head"]["repo"]["url"]}/tarball/${encodedHeadRef}` 141 | // This event triggers only for fork pull requests. We load the sha differently here. 142 | } else { 143 | const ref = process.env.GITHUB_SHA || process.env.GITHUB_REF_NAME || githubEvent?.repository?.master_branch 144 | if (!ref) { 145 | core.setFailed( 146 | `Could not guess the source code ref value. Neither a valid GitHub event or the GITHUB_REF_NAME env variable are available` 147 | ) 148 | } 149 | 150 | const url = githubEvent["repository"] 151 | ? githubEvent["repository"]["url"] 152 | : `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}` 153 | const encodedTarball = encodeURIComponent(ref) 154 | return `${url}/tarball/${encodedTarball}` 155 | } 156 | } catch (error) { 157 | core.warning(`Could not read content from ${eventPath}. Error: ${error}`) 158 | if (!process.env.GITHUB_SHA) { 159 | core.warning( 160 | "Could not find a valid GitHub SHA on environment. Is the GitHub action running as part of PR?" 161 | ) 162 | } else if (!process.env.GITHUB_REPOSITORY) { 163 | core.warning( 164 | "Could not find a valid GitHub Repository on environment. Is the GitHub action running as part of PR?" 165 | ) 166 | } else { 167 | return `https://github.com/${process.env.GITHUB_REPOSITORY}/archive/${process.env.GITHUB_SHA}.zip` 168 | } 169 | } 170 | } 171 | } 172 | 173 | export default ConfigurationFactory 174 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as core from "@actions/core" 2 | import Action from "./action" 3 | 4 | async function run(): Promise { 5 | try { 6 | const action = new Action(process.env.GITHUB_WORKSPACE || __dirname) 7 | await action.main() 8 | } catch (error) { 9 | if (error instanceof Error) { 10 | core.setFailed(error.message) 11 | } 12 | } 13 | } 14 | 15 | run() -------------------------------------------------------------------------------- /src/sanitize.ts: -------------------------------------------------------------------------------- 1 | const illegalRe = /[/?<>\\:*|"]/g 2 | //eslint-disable-next-line no-control-regex 3 | const controlRe = /[\x00-\x1f\x80-\x9f]/g 4 | const reservedRe = /^\.+$/ 5 | 6 | export function sanitize(input: string, replacement: string): string { 7 | if (typeof input !== "string") { 8 | throw new Error("Input must be string") 9 | } 10 | 11 | return input.replace(illegalRe, replacement).replace(controlRe, replacement).replace(reservedRe, replacement) 12 | } 13 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import * as core from "@actions/core" 2 | 3 | export function getNumberInput(name: string, value: number): number { 4 | const input = parseInt(core.getInput(name)) 5 | return isNaN(input) ? value : input 6 | } 7 | 8 | export function getNumberArray(name: string, defaultValues: number[]): number[] { 9 | const value = core.getInput(name) 10 | if (typeof value === "undefined" || value === "") { 11 | return defaultValues 12 | } 13 | 14 | try { 15 | const arrNums = JSON.parse(value) 16 | 17 | if (typeof arrNums === "object") { 18 | return arrNums.map(it => Number(it)) 19 | } else { 20 | return [Number.parseInt(arrNums)] 21 | } 22 | } catch (err) { 23 | core.debug(`Could not process ${name} value. ${err}`) 24 | core.warning(`Invalid value for ${name}. Using defaults.`) 25 | } 26 | return defaultValues 27 | } 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "outDir": "./lib", 6 | "rootDir": "./src", 7 | "strict": true, 8 | "noImplicitAny": false, 9 | "moduleResolution": "node", 10 | "allowSyntheticDefaultImports": true, 11 | "esModuleInterop": true, 12 | "declaration": false, 13 | "sourceMap": true, 14 | "lib": ["es6", "dom"] 15 | }, 16 | "exclude": ["node_modules", "__tests__"] 17 | } 18 | --------------------------------------------------------------------------------
Vulnerabilities
ActionArchitectureMinimalLowMediumHighCritical ℹ️UnknownResult
${infoMessage}
${action}${architecture}${passed}${skipped}${failed}${actionPassed ? "✅ " : "❌"}
${action}${architecture}${min}${low}${mid}${high}${critic}${unk}${passed ? "✅" : "❌"}