├── .eslintignore ├── .gitattributes ├── .gitignore ├── SECURITY.md ├── .github ├── workflows │ ├── build-and-publish.yml │ └── graphs │ │ └── build-and-publish.yml ├── PULL_REQUEST_TEMPLATE.md └── ISSUE_TEMPLATE.md ├── src ├── bundled_package.json └── main.ts ├── LICENSE.md ├── CONTRIBUTING.md ├── .eslintrc.json ├── package.json ├── action.yml ├── README.md ├── CODE_OF_CONDUCT.md └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | node_modules/ -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | hello-world-go-action filter=lfs diff=lfs merge=lfs -text 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __test__/_temp 2 | _temp/ 3 | lib/ 4 | node_modules/ 5 | .DS_Store -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | If you discover a vulnerability that is not suitable for public discussion, please send an email to [security@actionforge.dev](mailto:security@actionforge.dev). The email will be processed within 24 hours. If you prefer, you can also report security issues via GitHub Issues. -------------------------------------------------------------------------------- /.github/workflows/build-and-publish.yml: -------------------------------------------------------------------------------- 1 | on: [push] 2 | 3 | jobs: 4 | build-and-publish: 5 | runs-on: ubuntu-latest 6 | name: Execute action graph 7 | steps: 8 | - name: Execute action graph 9 | uses: actionforge/action@1cb7c397d2c7caa8d8071041976b6f2e4e2ead20 # v0.9.58 10 | with: 11 | graph_file: build-and-publish.yml -------------------------------------------------------------------------------- /src/bundled_package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "action", 3 | "binaries": { 4 | "linux-x64": "6bf6dc96a334b2f7a975ad6a8f06a67e49401d476088f618b7d9dafb07a390e1", 5 | "darwin-x64": "e60013321145db2365fe1bddbe6ba7f712de49d8af50936ba4877964617cb8c2", 6 | "darwin-arm64": "7bcf44be4979c154fd7f06905b6b6fe0e0f39272d1c44d981cbff2a71eedfce1", 7 | "win32-x64": "87db763ce2252fbb6e63f24ddadc2b4183f0bdfa39adbf1f1686d20b931c5247" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Contributing to Actionforge 2 | 3 | ❤️ First off, thanks for taking the time to contribute! ❤️ 4 | 5 | Before you create a pull request, please make sure you check the following: 6 | 7 | - [ ] A description of the problem you're trying to solve. 8 | - [ ] An overview of the suggested solution. 9 | - [ ] Your code should contain tests relevant for the problem you are solving. 10 | - [ ] If the feature changes current behavior, reasons why your solution is better. 11 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Actionforge Community License (ACL) Version 1.0.0 2 | 3 | This SOFTWARE is licensed under the Actionforge Community License that you can find [here](https://github.com/actionforge/legal/blob/main/LICENSE.md). 4 | 5 | Licenses for commercial use will soon be available on the GitHub Marketplace. 6 | 7 | For further information [Get in touch](mailto:hello@actionforge.dev). 8 | 9 | ## Definitions 10 | 11 | - **SOFTWARE**: Refers to both the source files and binaries included in the project. 12 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ❤️ Every contribution is welcome! Whether you're fixing a bug, adding new features, sharing an idea, or improving documentation, your help makes the project so much better. 4 | 5 | Please read the [Code of Conduct](CODE_OF_CONDUCT.md) before contributing. 6 | 7 | Other ways to contribute: 8 | 9 | - ⭐ Star [actionforge/action](https://github.com/actionforge/action) 10 | - 🐦 Follow Actionforge on X [@actionforge](https://twitter.com/actionforge) 11 | - 📝 Share what you think about Actionforge. 12 | 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ❤️ First off, thanks for taking the time to contribute! ❤️ 2 | 3 | ## Describe the bug or feature 4 | 5 | A clear and concise description of what the bug is. 6 | 7 | ## To reproduce a bug 8 | 9 | Steps to reproduce the behavior: 10 | 11 | 1. Go to '...' 12 | 2. Click on '....' 13 | 3. Scroll down to '....' 14 | 4. See error 15 | 16 | ## Expected behavior 17 | 18 | A clear and concise description of what you expected to happen. 19 | 20 | ## Screenshots 21 | 22 | If applicable, add screenshots to help explain your problem. 23 | 24 | ## GitHub Repo Link 25 | 26 | If applicable, add a link to the GitHub Actions workflow. 27 | 28 | ## Additional context 29 | 30 | Add any other context about the problem here. -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "project": [ 6 | "tsconfig.json" 7 | ] 8 | }, 9 | "overrides": [ 10 | { 11 | "files": [ 12 | "*.ts" 13 | ], 14 | "extends": [ 15 | "eslint:recommended", 16 | "plugin:@typescript-eslint/recommended" 17 | ], 18 | "rules": { 19 | "@typescript-eslint/no-unused-vars": [ 20 | "error", 21 | { 22 | "argsIgnorePattern": "^_", 23 | "varsIgnorePattern": "^_", 24 | "caughtErrorsIgnorePattern": "^_" 25 | } 26 | ], 27 | "@typescript-eslint/no-floating-promises": "error", 28 | "@typescript-eslint/explicit-function-return-type": [ 29 | "error", 30 | { 31 | "allowedNames": [ 32 | "ignoredFunctionName", 33 | "ignoredMethodName" 34 | ] 35 | } 36 | ], 37 | "@typescript-eslint/no-shadow": "error" 38 | } 39 | }, 40 | { 41 | "files": [ 42 | "*.html" 43 | ], 44 | "rules": {} 45 | } 46 | ] 47 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "action", 3 | "version": "0.9.58", 4 | "description": "", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "tsc && ncc build src/main.ts -o dist", 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "lint": "eslint src" 10 | }, 11 | "keywords": [ 12 | "github", 13 | "actions", 14 | "workflows", 15 | "node", 16 | "graph" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/actionforge/action.git" 21 | }, 22 | "author": "Actionforge", 23 | "license": "Actionforge Community License", 24 | "bugs": { 25 | "url": "https://github.com/actionforge/action/issues" 26 | }, 27 | "homepage": "https://github.com/actionforge/action#readme", 28 | "dependencies": { 29 | "@actions/core": "^1.10.1", 30 | "@actions/github": "^6.0.0", 31 | "@actions/tool-cache": "^2.0.1", 32 | "@octokit/action": "^7.0.0", 33 | "got": "^14.3.0" 34 | }, 35 | "devDependencies": { 36 | "@types/got": "^9.6.12", 37 | "@types/node": "^20.12.12", 38 | "@typescript-eslint/eslint-plugin": "^7.10.0", 39 | "@typescript-eslint/parser": "^7.10.0", 40 | "@vercel/ncc": "^0.38.1", 41 | "eslint": "^8.56.0", 42 | "typescript": "^5.4.5" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: Actionforge 2 | description: GitHub Action for executing an Actionforge Action Graph. 3 | inputs: 4 | graph_file: 5 | description: > 6 | The name of the graph file located in the `.github/workflows/graphs` directory. 7 | This file defines the Actionforge Action Graph to be executed. 8 | required: true 9 | token: 10 | description: > 11 | A Personal Access Token (PAT) used for authentication. 12 | If not provided, the default GitHub token for the repository will be used. 13 | 14 | [Learn more about creating and using 15 | encrypted secrets](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets) 16 | default: ${{ github.token }} 17 | inputs: 18 | description: > 19 | A JSON string containing the inputs to be passed to the Actionforge action graph. 20 | required: false 21 | default: '{}' 22 | secrets: 23 | description: > 24 | A JSON string containing the secrets to be passed to the Actionforge action graph. 25 | required: false 26 | default: '{}' 27 | matrix: 28 | description: > 29 | A JSON string containing the matrix to be passed to the Actionforge action graph. 30 | required: false 31 | default: '{}' 32 | runner_base_url: 33 | description: > 34 | Custom runner URL from which to obtain the graph runner binary. 35 | required: false 36 | default: '' 37 | runner_path: 38 | description: > 39 | Custom path to the graph runner binary to execute the action graph. 40 | required: false 41 | default: '' 42 | branding: 43 | icon: 'check' 44 | color: 'green' 45 | runs: 46 | using: 'node20' 47 | main: 'dist/index.js' 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![Actionforge Banner](https://www.actionforge.dev/assets/social.jpg?) 3 | 4 | # Actionforge Action 5 | 6 |
7 | 8 | [![view-action-graph](https://img.shields.io/github/actions/workflow/status/actionforge/action/build-and-publish.yml?label=View%20Action%20Graph)](https://www.actionforge.dev/github/actionforge/action/main/.github/workflows/graphs/build-and-publish.yml) 9 | [![made-with-ts](https://img.shields.io/badge/Made%20with-TS-3178C6.svg)](https://www.typescriptlang.org/) 10 | [![License](https://img.shields.io/badge/License-ACL-blue?color=orange)](https://www.github.com/actionforge/legal/blob/main/LICENSE.md) 11 | 12 |
13 | 14 | Welcome to the Actionforge Action source code! 15 | 16 | Actionforge introduces Action Graphs, a faster and easier way to create and execute GitHub workflows. This new approach replaces tedious YAML file editing by hand with a user-friendly graph editor. Action Graphs are compatible with GitHub Actions. The core product consists out of 4 components: 17 | 18 | - ♾️ [VS Code Extension](https://www.github.com/actionforge/vscode-ext) - Extension to modify Action Graphs within VS Code. 19 | - 🟢 [GitHub Action](https://www.github.com/actionforge/action) - GitHub Action that reads the Action Graph and starts the Graph Runner with it. 20 | - 🏃‍♀️ [Graph Runner](https://www.github.com/actionforge/graph-runner) - The cli program that executes an Action Graph. 21 | - 🕸️ [Graph Editor](https://www.github.com/actionforge/graph-editor) - Visual graph editor to create and build Action Graphs. These graphs will be committed to your repository. 22 | 23 | For a full introduction check out the [Actionforge Documentation](https://www.actionforge.dev/docs). 24 | 25 | ## Usage 26 | 27 | 28 | ```yaml 29 | - uses: actionforge/action@1cb7c397d2c7caa8d8071041976b6f2e4e2ead20 # v0.9.58 30 | with: 31 | # The name of the graph file located in the `.github/workflows/graphs` directory. 32 | # This file defines the Actionforge Action Graph to be executed. 33 | graph_file: '' 34 | 35 | # A Personal Access Token (PAT) used for authentication. 36 | # If not provided, the default GitHub token for the repository will be used. 37 | # Default: ${{ github.token }} 38 | # 39 | # [Learn more about creating and using encrypted secrets](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets) 40 | token: '' 41 | ``` 42 | 43 | For examples check out the [Actionforge Examples](https://www.github.com/actionforge/examples) 🔗 repository. 44 | 45 | ## License 46 | 47 | This SOFTWARE is licensed under the Actionforge Community License that you can find [here](https://github.com/actionforge/legal/blob/main/LICENSE.md). 48 | 49 | Licenses for commercial use will soon be available on the GitHub Marketplace. 50 | 51 | For further information [Get in touch](mailto:hello@actionforge.dev). 52 | -------------------------------------------------------------------------------- /.github/workflows/graphs/build-and-publish.yml: -------------------------------------------------------------------------------- 1 | entry: gh-start 2 | executions: 3 | - src: 4 | node: github-com-actions-setup-node-cherry-grape-banana 5 | port: exec 6 | dst: 7 | node: run-v1-strawberry-cat-blueberry 8 | port: exec 9 | - src: 10 | node: gh-start 11 | port: exec-on-push 12 | dst: 13 | node: github-com-actions-checkout-coconut-coconut-banana 14 | port: exec 15 | - src: 16 | node: github-com-actions-checkout-coconut-coconut-banana 17 | port: exec 18 | dst: 19 | node: branch-v1-orange-giraffe-lion 20 | port: exec 21 | - src: 22 | node: branch-v1-orange-giraffe-lion 23 | port: exec-then 24 | dst: 25 | node: run-v1-brown-brown-panda 26 | port: exec 27 | - src: 28 | node: run-v1-brown-brown-panda 29 | port: exec-success 30 | dst: 31 | node: github-com-actions-setup-node-cherry-grape-banana 32 | port: exec 33 | - src: 34 | node: branch-v1-orange-giraffe-lion 35 | port: exec-otherwise 36 | dst: 37 | node: github-com-actions-setup-node-cherry-grape-banana 38 | port: exec 39 | connections: 40 | - src: 41 | node: env-get-v1-grape-tiger-cherry 42 | port: env 43 | dst: 44 | node: string-match-v1-panda-orange-gold 45 | port: str1 46 | - src: 47 | node: string-match-v1-panda-orange-gold 48 | port: result 49 | dst: 50 | node: branch-v1-orange-giraffe-lion 51 | port: condition 52 | nodes: 53 | - id: gh-start 54 | type: gh-start@v1 55 | position: 56 | x: -380 57 | y: 150 58 | settings: 59 | folded: false 60 | - id: github-com-actions-checkout-coconut-coconut-banana 61 | type: github.com/actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 62 | position: 63 | x: 130 64 | y: 50 65 | settings: 66 | folded: false 67 | - id: github-com-actions-setup-node-cherry-grape-banana 68 | type: github.com/actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 69 | position: 70 | x: 1360 71 | y: 110 72 | inputs: 73 | node-version: v20.10.0 74 | settings: 75 | folded: false 76 | - id: run-v1-strawberry-cat-blueberry 77 | type: run@v1 78 | position: 79 | x: 1920 80 | y: 90 81 | inputs: 82 | script: |- 83 | npm install 84 | npm run lint 85 | settings: 86 | folded: false 87 | - id: run-v1-brown-brown-panda 88 | type: run@v1 89 | position: 90 | x: 950 91 | y: -130 92 | inputs: 93 | script: |- 94 | # Ensure the version in package.json matches the current git tag 95 | PACKAGE_VERSION=$(jq -r '.version' package.json) 96 | TAG_VERSION=${GITHUB_REF_NAME#v} 97 | 98 | if [ "$PACKAGE_VERSION" != "$TAG_VERSION" ]; then 99 | echo "Version mismatch: package.json version is $PACKAGE_VERSION, but tag is v$TAG_VERSION" 100 | exit 1 101 | fi 102 | settings: 103 | folded: false 104 | - id: env-get-v1-grape-tiger-cherry 105 | type: env-get@v1 106 | position: 107 | x: -70 108 | y: 1090 109 | inputs: 110 | env: GITHUB_REF 111 | settings: 112 | folded: false 113 | - id: string-match-v1-panda-orange-gold 114 | type: string-match@v1 115 | position: 116 | x: 270 117 | y: 930 118 | inputs: 119 | op: startswith 120 | str2: refs/tags/ 121 | settings: 122 | folded: false 123 | - id: branch-v1-orange-giraffe-lion 124 | type: branch@v1 125 | position: 126 | x: 680 127 | y: 220 128 | settings: 129 | folded: false 130 | registries: [] 131 | description: '' 132 | -------------------------------------------------------------------------------- /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 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 63 | hello@actionforge.dev. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 7 | // "allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 10 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 11 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 12 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 13 | // "outFile": "./", /* Concatenate and emit output to single file. */ 14 | "outDir": "./lib", /* Redirect output structure to the directory. */ 15 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 16 | // "composite": true, /* Enable project compilation */ 17 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 18 | // "removeComments": true, /* Do not emit comments to output. */ 19 | // "noEmit": true, /* Do not emit outputs. */ 20 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 21 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 22 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 23 | 24 | /* Strict Type-Checking Options */ 25 | "strict": true, /* Enable all strict type-checking options. */ 26 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 27 | // "strictNullChecks": true, /* Enable strict null checks. */ 28 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 29 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 30 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 31 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 32 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 33 | 34 | /* Additional Checks */ 35 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 36 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 37 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 38 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 39 | 40 | /* Module Resolution Options */ 41 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 42 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 43 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 44 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 45 | // "typeRoots": [], /* List of folders to include type definitions from. */ 46 | // "types": [], /* Type declaration files to be included in compilation. */ 47 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 48 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 49 | "resolveJsonModule": true, /* Allows importing modules with a '.json' extension, which is a common practice in node projects. */ 50 | "sourceMap": true, 51 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 52 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 53 | 54 | /* Source Map Options */ 55 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 56 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 57 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 58 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 59 | 60 | /* Experimental Options */ 61 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 62 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 63 | }, 64 | "exclude": ["__test__", "lib", "node_modules"] 65 | } 66 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import * as core from "@actions/core"; 2 | import * as github from "@actions/github"; 3 | import * as stream from "stream"; 4 | import * as util from "util"; 5 | import * as crypto from "crypto"; 6 | import * as tc from "@actions/tool-cache"; 7 | import * as pjdata from './bundled_package.json'; 8 | import * as pjson from './../package.json'; 9 | 10 | import os from "os"; 11 | import path from "path"; 12 | import fs from "fs"; 13 | import got from "got"; 14 | import cp from "child_process"; 15 | 16 | const packageJson: Record = pjson as unknown as Record; 17 | const pj: BundledPackage = pjdata as unknown as BundledPackage; 18 | 19 | type BundledPackage = { 20 | name: string; 21 | binaries: Record; 22 | }; 23 | 24 | const debugOutput = process.env.ACTIONS_STEP_DEBUG === 'true'; 25 | 26 | /** 27 | * Represents information about a runner version, 28 | * including its download URL, resolved version, and file name. 29 | */ 30 | export interface IRunnerVersionInfo { 31 | downloadUrl: string; 32 | filename: string; 33 | } 34 | 35 | /** 36 | * Extracts the contents of an archive file to a directory. 37 | * @param archivePath The path to the archive file. 38 | * @returns A Promise that resolves to the path of the extracted directory. 39 | */ 40 | export async function extractArchive(archivePath: string): Promise { 41 | if (archivePath.endsWith(".tar.gz")) { 42 | return tc.extractTar(archivePath); 43 | } else if (archivePath.endsWith(".zip")) { 44 | return tc.extractZip(archivePath); 45 | } else { 46 | throw new Error(`Unsupported archive format: ${archivePath}`); 47 | } 48 | } 49 | 50 | /** 51 | * Calculates the SHA256 hash of a file. 52 | * @param filePath The path to the file. 53 | * @returns A Promise that resolves to the hash. 54 | */ 55 | export async function calculateFileHash(filePath: string): Promise { 56 | return new Promise((resolve, reject) => { 57 | const hash = crypto.createHash('sha256'); 58 | const s = fs.createReadStream(filePath); 59 | 60 | s.on('data', (data) => hash.update(data)); 61 | s.on('end', () => resolve(hash.digest('hex'))); 62 | s.on('error', (err) => reject(err)); 63 | }); 64 | } 65 | 66 | /** 67 | * Downloads the specified runner version and returns the path to the executable. 68 | * @param info - The runner version information. 69 | * @returns The path to the executable. 70 | */ 71 | async function downloadRunner(info: IRunnerVersionInfo, token: string | null, hashCheck: boolean): Promise { 72 | const tempDir = process.env.RUNNER_TEMP || '.'; 73 | const zipPath = path.join(tempDir, info.filename + '.zip'); 74 | if (debugOutput) { 75 | console.log(`📥 Downloaded zip to ${zipPath} from ${info.downloadUrl} 🚀`); 76 | } 77 | 78 | const pipeline = util.promisify(stream.pipeline); 79 | await pipeline( 80 | got.stream(info.downloadUrl, { 81 | method: "GET", 82 | headers: { 83 | "Accept": "application/octet-stream", 84 | "User-Agent": "GitHub Actions", 85 | ...(token ? { 86 | "Authorization": `token ${token}` } : {} 87 | ), 88 | }, 89 | }), 90 | fs.createWriteStream(zipPath) 91 | ); 92 | 93 | const zipDstPath = await extractArchive(zipPath); 94 | if (debugOutput) { 95 | console.log(`📦 Extracted runner to ${zipDstPath}`); 96 | } 97 | 98 | let binPath = path.join(zipDstPath, info.filename); 99 | if (os.platform() === "linux" || os.platform() === "darwin") { 100 | fs.chmodSync(binPath, 0o755); 101 | } else { 102 | binPath = binPath + ".exe"; 103 | } 104 | 105 | if (hashCheck) { 106 | const hash = await calculateFileHash(binPath); 107 | if (hash.length !== 64 || hash !== pj.binaries[`${os.platform()}-${os.arch()}`]) { 108 | throw new Error(`Hash mismatch for ${binPath}`); 109 | } 110 | } 111 | 112 | return binPath; 113 | } 114 | 115 | /** 116 | * Executes a runner with the specified runnerPath and sets up the required environment variables. 117 | * @param runnerPath The path to the runner executable. 118 | * @param graphFile The path to the graph file. 119 | * @returns A Promise that resolves when the runner has finished executing. 120 | */ 121 | async function executeRunner( 122 | runnerPath: string, 123 | graphFile: string, 124 | inputs: string, 125 | matrix: string, 126 | secrets: string, 127 | ): Promise { 128 | const token = core.getInput("token"); 129 | 130 | const octokit = github.getOctokit(token); 131 | 132 | const { data } = await octokit.rest.repos.getContent({ 133 | owner: github.context.repo.owner, 134 | repo: github.context.repo.repo, 135 | ref: github.context.sha, 136 | path: graphFile, 137 | }); 138 | 139 | fs.mkdirSync(path.dirname(graphFile), { recursive: true }); 140 | const buf = Buffer.from((data as { content: string }).content, "base64"); 141 | fs.writeFileSync(graphFile, buf.toString("utf-8")); 142 | 143 | const customEnv = { 144 | ...process.env, 145 | GRAPH_FILE: graphFile, 146 | INPUT_MATRIX: matrix, 147 | INPUT_INPUTS: inputs, 148 | INPUT_SECRETS: secrets, 149 | }; 150 | console.log(`🟢 Running graph-runner`, graphFile); 151 | cp.execSync(runnerPath, { stdio: "inherit", env: customEnv }); 152 | } 153 | 154 | /** 155 | * Asserts that the given object is an empty or non-empty string. 156 | */ 157 | function assertValidString(o?: string | null): string { 158 | if (o === null || o === undefined || o === "" || o === "null" || o === "{}") { 159 | return ""; 160 | } else { 161 | return o; 162 | } 163 | } 164 | 165 | /** 166 | * The main function that downloads and installs the runner. 167 | */ 168 | async function run(): Promise { 169 | let runnerPath: string = core.getInput("runner_path", { trimWhitespace: true }); 170 | 171 | const runnerBaseUrl: string = core.getInput("runner_base_url", { trimWhitespace: true }); 172 | const inputs: string = assertValidString(core.getInput("inputs")); 173 | const matrix: string = assertValidString(core.getInput("matrix")); 174 | const secrets: string = assertValidString(core.getInput("secrets")); 175 | 176 | const token = core.getInput("token"); 177 | if (!token) { 178 | throw new Error(`No GitHub token found`) 179 | } 180 | 181 | const GRAPH_FILE_DIR = ".github/workflows/graphs"; 182 | let graphFile = path.join( 183 | GRAPH_FILE_DIR, 184 | core.getInput("graph_file", { required: true }) 185 | ); 186 | if (os.platform() === "win32") { 187 | graphFile = graphFile.replace(/\\/g, "/"); 188 | } 189 | 190 | // Log output 191 | const sha = process.env.GITHUB_SHA; 192 | const repo = process.env.GITHUB_REPOSITORY; 193 | const runId = process.env.GITHUB_RUN_ID; 194 | const output = `🟢 View Action Graph: https://www.actionforge.dev/github/${repo}/${sha}/${graphFile}?run_id=${runId}`; 195 | const delimiter = '-'.repeat(32); 196 | console.log(`${delimiter}`); 197 | console.log(output); 198 | console.log(`${delimiter}`); 199 | 200 | if (runnerPath) { 201 | console.log("\u27a1 Custom runner path set:", runnerPath); 202 | } else { 203 | const baseUrl = `https://github.com/actionforge/${pj.name}/releases/download/v${packageJson.version}`; 204 | const downloadUrl = `${runnerBaseUrl.replace(/\/$/, "") || baseUrl}/graph-runner-${os.platform()}-${os.arch()}.zip`; 205 | if (runnerBaseUrl) { 206 | console.log("\u27a1 Custom runner URL set:", downloadUrl); 207 | } 208 | 209 | const downloadInfo = { 210 | downloadUrl: downloadUrl, 211 | filename: `graph-runner-${os.platform()}-${os.arch()}`, 212 | }; 213 | 214 | runnerPath = await downloadRunner(downloadInfo, runnerBaseUrl ? null : token, runnerBaseUrl ? false : true); 215 | } 216 | 217 | return executeRunner(runnerPath, graphFile, inputs, matrix, secrets); 218 | } 219 | 220 | async function main(): Promise { 221 | try { 222 | await run(); 223 | } catch (error) { 224 | if (error instanceof Error) core.setFailed(error.message); 225 | return process.exit(1); 226 | } 227 | } 228 | 229 | void main(); 230 | --------------------------------------------------------------------------------