├── .github ├── CODEOWNERS ├── actions │ └── attach-release-assets │ │ ├── Dockerfile │ │ ├── action.yml │ │ └── run.sh ├── pull_request_template.md └── workflows │ ├── ci.yml │ ├── create-release.yml │ └── release.yml ├── Cargo.toml ├── mask-parser ├── src │ ├── lib.rs │ ├── maskfile.rs │ └── parser.rs ├── README.md └── Cargo.toml ├── .gitignore ├── LICENSE ├── mask ├── Cargo.toml ├── tests │ ├── common │ │ └── mod.rs │ ├── introspect_test.rs │ ├── env_vars_test.rs │ ├── subcommands_test.rs │ ├── integration_test.rs │ ├── supported_runtimes_test.rs │ └── arguments_and_flags_test.rs └── src │ ├── loader.rs │ ├── executor.rs │ └── main.rs ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md ├── maskfile.md ├── CHANGELOG.md ├── README.md └── Cargo.lock /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @jacobdeichert 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "mask", 4 | "mask-parser", 5 | ] 6 | -------------------------------------------------------------------------------- /mask-parser/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod maskfile; 2 | mod parser; 3 | 4 | pub use parser::parse; 5 | -------------------------------------------------------------------------------- /.github/actions/attach-release-assets/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.10 2 | 3 | RUN apk add --no-cache bash curl ca-certificates jq 4 | 5 | COPY run.sh /run.sh 6 | 7 | ENTRYPOINT ["/run.sh"] 8 | -------------------------------------------------------------------------------- /mask-parser/README.md: -------------------------------------------------------------------------------- 1 | # mask-parser 2 | 3 | A parser for the [maskfile.md][mask] format. 4 | 5 | This library is not yet stable and subject to change. 6 | 7 | [mask]: https://github.com/jacobdeichert/mask 8 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | ### Which issue does this fix? 6 | 7 | 8 | Closes #{ISSUE} 9 | 10 | 11 | 12 | ### Describe the solution 13 | 14 | -------------------------------------------------------------------------------- /.github/actions/attach-release-assets/action.yml: -------------------------------------------------------------------------------- 1 | name: attach-release-assets 2 | description: Attaches files/assets/binaries to a GitHub Release 3 | author: Jacob Deichert 4 | branding: 5 | icon: file-plus 6 | color: orange 7 | inputs: 8 | assets: 9 | description: A file glob of assets to be attached to the release 10 | required: true 11 | runs: 12 | using: docker 13 | image: Dockerfile 14 | args: 15 | - ${{ inputs.assets }} 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ####################################### 2 | # system 3 | ####################################### 4 | *.DS_Store 5 | *.DS_Store? 6 | *._* 7 | *.Spotlight-V100 8 | *.Trashes 9 | Icon? 10 | ehthumbs.db 11 | Thumbs.db 12 | Desktop.ini 13 | $RECYCLE.BIN/ 14 | 15 | ####################################### 16 | # rust/cargo 17 | ####################################### 18 | /target 19 | **/*.rs.bk 20 | 21 | ####################################### 22 | # node 23 | ####################################### 24 | package-lock.json 25 | 26 | # Logs 27 | logs 28 | *.log 29 | 30 | # Runtime data 31 | pids 32 | *.pid 33 | *.seed 34 | 35 | # Dependency directory 36 | node_modules 37 | -------------------------------------------------------------------------------- /mask-parser/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mask-parser" 3 | version = "0.2.2" 4 | description = "A parser for the maskfile.md format" 5 | authors = ["Jacob Deichert "] 6 | repository = "https://github.com/jacobdeichert/mask" 7 | keywords = ["cli", "task", "command", "maskfile", "markdown"] 8 | categories = ["command-line-interface", "command-line-utilities", "development-tools::build-utils", "parser-implementations"] 9 | edition = "2018" 10 | license = "MIT" 11 | 12 | [dependencies] 13 | pulldown-cmark = { version = "0.5", default-features = false } # https://github.com/raphlinus/pulldown-cmark 14 | serde = { version = "1.0", features = ["derive"] } # https://github.com/serde-rs/serde 15 | serde_json = "1.0" # https://github.com/serde-rs/json 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Jacob Deichert 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 15 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 16 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 18 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 19 | OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /.github/actions/attach-release-assets/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [[ -z "$GITHUB_TOKEN" ]]; then 3 | echo "ERROR: the GITHUB_TOKEN env variable wasn't set" 4 | exit 1 5 | fi 6 | 7 | # A file glob of assets to upload. The docker entrypoint arg is "inputs.assets". 8 | ASSET_GLOBS=($1) 9 | AUTH_HEADER="Authorization: token ${GITHUB_TOKEN}" 10 | RELEASE_ID=$(jq --raw-output '.release.id' "$GITHUB_EVENT_PATH") 11 | 12 | # Upload each asset file to the GitHub Release 13 | for asset_file in "${ASSET_GLOBS[@]}"; do 14 | filename=$(basename "$asset_file") 15 | upload_url="https://uploads.github.com/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}/assets?name=${filename}" 16 | 17 | echo "Uploading asset: $asset_file" 18 | 19 | touch curl_log 20 | response_code=$(curl \ 21 | -sSL \ 22 | -XPOST \ 23 | -H "${AUTH_HEADER}" \ 24 | --upload-file "${asset_file}" \ 25 | --header "Content-Type:application/octet-stream" \ 26 | --write-out "%{http_code}" \ 27 | --output curl_log \ 28 | "$upload_url") 29 | 30 | if [ $response_code -ge 400 ]; then 31 | echo "ERROR: curl upload failed with status code $response_code" 32 | cat curl_log && rm curl_log 33 | exit 1 34 | fi 35 | done 36 | -------------------------------------------------------------------------------- /mask/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mask" 3 | version = "0.11.6" 4 | description = "A CLI task runner defined by a simple markdown file" 5 | authors = ["Jacob Deichert "] 6 | repository = "https://github.com/jacobdeichert/mask" 7 | readme = "../README.md" 8 | keywords = ["cli", "task", "command", "make", "markdown"] 9 | categories = ["command-line-interface", "command-line-utilities", "development-tools::build-utils", "parser-implementations"] 10 | edition = "2018" 11 | license = "MIT" 12 | 13 | [dependencies] 14 | colored = "2" # https://github.com/mackwic/colored 15 | serde_json = "1.0" # https://github.com/serde-rs/json 16 | mask-parser = { path = "../mask-parser", version = "0.2" } 17 | 18 | [dependencies.clap] # https://github.com/clap-rs/clap 19 | version = "2.33" 20 | features = ["wrap_help"] 21 | 22 | [dev-dependencies] 23 | assert_cmd = "1" # https://github.com/assert-rs/assert_cmd 24 | assert_fs = "1" # https://github.com/assert-rs/assert_fs 25 | predicates = "1" # https://github.com/assert-rs/predicates-rs 26 | -------------------------------------------------------------------------------- /mask/tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | use assert_cmd::{crate_name, prelude::*}; 2 | use assert_fs::prelude::*; 3 | use std::path::PathBuf; 4 | use std::process::Command; 5 | 6 | pub trait MaskCommandExt { 7 | fn command(&mut self, c: &'static str) -> &mut Command; 8 | fn cli(&mut self, arguments: &'static str) -> &mut Command; 9 | } 10 | 11 | impl MaskCommandExt for Command { 12 | fn command(&mut self, c: &'static str) -> &mut Command { 13 | self.arg(c); 14 | self 15 | } 16 | 17 | fn cli(&mut self, arguments: &'static str) -> &mut Command { 18 | let args: Vec<&str> = arguments.split(" ").collect(); 19 | for arg in args { 20 | self.arg(arg); 21 | } 22 | self 23 | } 24 | } 25 | 26 | pub fn maskfile(content: &'static str) -> (assert_fs::TempDir, PathBuf) { 27 | let temp = assert_fs::TempDir::new().unwrap(); 28 | let maskfile = temp.child("maskfile.md"); 29 | 30 | maskfile.write_str(content).unwrap(); 31 | 32 | let maskfile_path = maskfile.path().to_path_buf(); 33 | 34 | (temp, maskfile_path) 35 | } 36 | 37 | pub fn run_mask(maskfile: &PathBuf) -> Command { 38 | let mut mask = Command::cargo_bin(crate_name!()).expect("Was not able to find binary"); 39 | 40 | mask.arg("--maskfile") 41 | .arg(maskfile) 42 | // Force "colored" to output colored text for tests 43 | .env("CLICOLOR_FORCE", "1"); 44 | 45 | mask 46 | } 47 | -------------------------------------------------------------------------------- /mask/src/loader.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::prelude::*; 3 | use std::path::Path; 4 | 5 | pub fn read_maskfile(maskfile: &Path) -> Result { 6 | let file = File::open(maskfile); 7 | if file.is_err() { 8 | return Err("failed to open maskfile.md".to_string()); 9 | } 10 | 11 | let mut file = file.unwrap(); 12 | let mut maskfile_contents = String::new(); 13 | file.read_to_string(&mut maskfile_contents) 14 | .expect("expected file contents"); 15 | 16 | Ok(maskfile_contents) 17 | } 18 | 19 | #[cfg(test)] 20 | mod read_maskfile { 21 | use super::*; 22 | 23 | #[test] 24 | fn reads_root_maskfile() { 25 | let maskfile = read_maskfile(Path::new("../maskfile.md")); 26 | 27 | assert!(maskfile.is_ok(), "maskfile was ok"); 28 | 29 | let contents = maskfile.unwrap(); 30 | 31 | // Basic test to make sure the maskfile.md contents are at least right 32 | let expected_root_description = "Development tasks for mask."; 33 | assert!( 34 | contents.contains(expected_root_description), 35 | "description wasn't found in maskfile contents" 36 | ); 37 | } 38 | 39 | #[test] 40 | fn errors_for_non_existent_maskfile() { 41 | let maskfile = read_maskfile(Path::new("src/maskfile.md")); 42 | 43 | assert!(maskfile.is_err(), "maskfile was err"); 44 | 45 | let err = maskfile.unwrap_err(); 46 | 47 | let expected_err = "failed to open maskfile.md"; 48 | assert_eq!(err, expected_err, "error message was wrong"); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | 4 | 5 | 6 | 7 | ## Reporting Bugs or Suggesting Features 8 | 9 | Please file an issue for discussion of new features or bugs. If an existing issue already exists, please comment there instead if you have more details to provide. 10 | 11 | If you're logging a bug, please be as detailed as possible. It's highly recommended that you post a minimum `maskfile.md` that reproduces the problem so we can debug it more easily. Issues may be closed if you don't provide enough info. 12 | 13 | 14 | 15 | 16 | 17 | ## Running the test suite 18 | 19 | After you clone `mask` you will need to [install](https://github.com/jacobdeichert/mask#installation) it to successfully run the test suite. 20 | 21 | Once you have `mask` installed, you can then run `mask link` and `mask test` to run the entire test suite. This will ensure all test cases are running against the latest modifications you've made to the `mask` source. 22 | 23 | 24 | 25 | 26 | 27 | ## Submitting Pull Requests 28 | 29 | If you're tackling a larger feature or bug, please leave a comment on the corresponding issue unless someone before you already has indicated so. If there is no issue yet, please create one so it can be discussed, before you start working on a PR. This is to save you time and energy, and to ensure we're all on the same page with the chosen solution! 🙂 30 | 31 | Adding tests is highly recommended, though not always necessary depending on the scope of changes made. 32 | 33 | 34 | 35 | 36 | 37 | ## Code of Conduct 38 | 39 | Please review and follow the rules within our [Code of Conduct](CODE_OF_CONDUCT.md). 40 | -------------------------------------------------------------------------------- /mask/tests/introspect_test.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | use assert_cmd::prelude::*; 3 | use predicates::str::contains; 4 | use serde_json::json; 5 | 6 | #[test] 7 | fn outputs_the_maskfile_structure_as_json() { 8 | let (_temp, maskfile_path) = common::maskfile( 9 | r#" 10 | # Document Title 11 | 12 | ## somecommand 13 | > The command description 14 | 15 | ~~~bash 16 | echo something 17 | ~~~ 18 | "#, 19 | ); 20 | 21 | let verbose_flag = json!({ 22 | "name": "verbose", 23 | "description": "Sets the level of verbosity", 24 | "short": "v", 25 | "long": "verbose", 26 | "multiple": false, 27 | "takes_value": false, 28 | "required": false, 29 | "validate_as_number": false, 30 | "choices": [], 31 | }); 32 | 33 | let expected_json = json!({ 34 | "title": "Document Title", 35 | "description": "", 36 | "commands": [ 37 | { 38 | "level": 2, 39 | "name": "somecommand", 40 | "description": "The command description", 41 | "script": { 42 | "executor": "bash", 43 | "source": "echo something\n", 44 | }, 45 | "subcommands": [], 46 | "required_args": [], 47 | "optional_args": [], 48 | "named_flags": [verbose_flag], 49 | } 50 | ] 51 | }); 52 | 53 | common::run_mask(&maskfile_path) 54 | .arg("--introspect") 55 | .assert() 56 | .code(0) 57 | .stdout(contains( 58 | serde_json::to_string_pretty(&expected_json).unwrap(), 59 | )) 60 | .success(); 61 | } 62 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | name: ${{ matrix.platform }}-build 8 | runs-on: ${{ matrix.platform }} 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | platform: [ubuntu-latest, windows-latest, macos-latest] 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Fetch dependencies 16 | run: cargo fetch 17 | - name: Build in release mode 18 | run: cargo build --release --frozen 19 | 20 | test: 21 | name: ${{ matrix.platform }}-test 22 | runs-on: ${{ matrix.platform }} 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | # Test on macos-13 because the macos-14 runner doesn't come with php installed. 27 | platform: [ubuntu-latest, windows-latest, macos-13] 28 | env: 29 | CLICOLOR_FORCE: 1 30 | steps: 31 | - uses: actions/checkout@v4 32 | - name: Add Ruby for a test that requires it 33 | uses: ruby/setup-ruby@v1 34 | with: 35 | ruby-version: 2.6 36 | - name: Fetch dependencies 37 | run: cargo fetch 38 | - name: Build in test mode 39 | run: cargo build --tests --frozen 40 | - name: Make mask available globally (windows) 41 | run: copy ./target/debug/mask.exe ~/.cargo/bin/ 42 | if: matrix.platform == 'windows-latest' 43 | - name: Make mask available globally (linux / mac) 44 | run: cp ./target/debug/mask ~/.cargo/bin 45 | if: matrix.platform != 'windows-latest' 46 | - name: Run tests 47 | run: cargo test --frozen 48 | 49 | format: 50 | runs-on: ubuntu-latest 51 | steps: 52 | - uses: actions/checkout@v4 53 | - name: Verify formatting is correct 54 | run: cargo fmt --all -- --check 55 | -------------------------------------------------------------------------------- /mask/tests/env_vars_test.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | use assert_cmd::prelude::*; 3 | use common::MaskCommandExt; 4 | use predicates::str::contains; 5 | 6 | // NOTE: This test suite depends on the mask binary being available in the current shell 7 | 8 | // Using current_dir("tests") to make sure the default maskfile.md can't be found 9 | mod env_var_mask { 10 | use super::*; 11 | 12 | #[test] 13 | fn works_from_any_dir() { 14 | let (_temp, maskfile_path) = common::maskfile( 15 | r#" 16 | ## ci 17 | 18 | ~~~bash 19 | $MASK test 20 | ~~~ 21 | 22 | ~~~powershell 23 | $path = $env:MASK.replace("\\?\", "") 24 | $pos = $path.IndexOf(" "); 25 | $arglist = $path.Substring($pos + 1); 26 | 27 | Start-Process mask.exe -ArgumentList "$arglist test" -wait -NoNewWindow -PassThru 28 | ~~~ 29 | 30 | ## test 31 | 32 | ~~~bash 33 | echo "tests passed" 34 | ~~~ 35 | 36 | ~~~powershell 37 | Write-Output "tests passed" 38 | ~~~ 39 | "#, 40 | ); 41 | 42 | common::run_mask(&maskfile_path) 43 | .current_dir("tests") 44 | .command("ci") 45 | .assert() 46 | .stdout(contains("tests passed")) 47 | .success(); 48 | } 49 | 50 | #[test] 51 | fn set_to_the_correct_value() { 52 | let (_temp, maskfile_path) = common::maskfile( 53 | r#" 54 | ## run 55 | 56 | ~~~bash 57 | echo "mask = $MASK" 58 | ~~~ 59 | 60 | ~~~powershell 61 | param ( 62 | $var = "$env:mask /" 63 | ) 64 | 65 | Write-Output "mask = $var" 66 | ~~~ 67 | 68 | "#, 69 | ); 70 | 71 | #[cfg(windows)] 72 | let predicate = contains("mask = mask --maskfile \\"); 73 | #[cfg(not(windows))] 74 | let predicate = contains("mask = mask --maskfile /"); 75 | 76 | common::run_mask(&maskfile_path) 77 | .current_dir("tests") 78 | .command("run") 79 | .assert() 80 | // Absolute maskfile path starts with / 81 | .stdout(predicate) 82 | // And ends with maskfile.md 83 | .stdout(contains("maskfile.md")) 84 | .success(); 85 | } 86 | } 87 | 88 | // Using current_dir("tests) to make sure the default maskfile.md can't be found 89 | mod env_var_maskfile_dir { 90 | use super::*; 91 | 92 | #[test] 93 | fn set_to_the_correct_value() { 94 | let (_temp, maskfile_path) = common::maskfile( 95 | r#" 96 | ## run 97 | 98 | ~~~bash 99 | echo "maskfile_dir = $MASKFILE_DIR" 100 | ~~~ 101 | 102 | ~~~powershell 103 | param ( 104 | $var = $env:maskfile_dir 105 | ) 106 | 107 | Write-Output "maskfile_dir = /$var" 108 | ~~~ 109 | "#, 110 | ); 111 | 112 | common::run_mask(&maskfile_path) 113 | .current_dir("tests") 114 | .command("run") 115 | .assert() 116 | // Absolute maskfile path starts with / 117 | .stdout(contains("maskfile_dir = /")) 118 | .success(); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /mask/tests/subcommands_test.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | use assert_cmd::prelude::*; 3 | use colored::*; 4 | use common::MaskCommandExt; 5 | use predicates::str::contains; 6 | 7 | #[test] 8 | fn positional_arguments() { 9 | let (_temp, maskfile_path) = common::maskfile( 10 | r#" 11 | 12 | ## services 13 | 14 | > Commands related to starting, stopping, and restarting services 15 | 16 | ### services start (service_name) 17 | 18 | > Start a service. 19 | 20 | ~~~bash 21 | echo "Starting service $service_name" 22 | ~~~ 23 | 24 | ~~~powershell 25 | param( 26 | $service_name = $env:service_name 27 | ) 28 | 29 | Write-Output "Starting service $service_name" 30 | ~~~ 31 | 32 | ### services stop (service_name) 33 | 34 | > Stop a service. 35 | 36 | ~~~bash 37 | echo "Stopping service $service_name" 38 | ~~~ 39 | 40 | ~~~powershell 41 | param( 42 | $service_name = $service_name 43 | ) 44 | 45 | Write-Output "Stopping service $service_name" 46 | ~~~ 47 | "#, 48 | ); 49 | 50 | common::run_mask(&maskfile_path) 51 | .cli("services start my_fancy_service") 52 | .assert() 53 | .stdout(contains("Starting service my_fancy_service")) 54 | .success(); 55 | } 56 | 57 | #[test] 58 | fn exits_with_error_when_missing_subcommand() { 59 | let (_temp, maskfile_path) = common::maskfile( 60 | r#" 61 | ## service 62 | ### service start 63 | 64 | ~~~bash 65 | echo "subcommand should exist" 66 | ~~~ 67 | "#, 68 | ); 69 | 70 | #[cfg(not(windows))] 71 | let predicate = 72 | contains("error: 'mask service' requires a subcommand, but one was not provided"); 73 | #[cfg(windows)] 74 | let predicate = 75 | contains("error: 'mask.exe service' requires a subcommand, but one was not provided"); 76 | common::run_mask(&maskfile_path) 77 | .command("service") 78 | .assert() 79 | .code(1) 80 | .stderr(predicate) 81 | .failure(); 82 | } 83 | 84 | mod when_command_has_no_source { 85 | use super::*; 86 | 87 | #[test] 88 | fn exits_with_error_when_it_has_no_script_lang_code() { 89 | let (_temp, maskfile_path) = common::maskfile( 90 | r#" 91 | ## start 92 | ~~~ 93 | echo "system, online" 94 | ~~~ 95 | "#, 96 | ); 97 | 98 | common::run_mask(&maskfile_path) 99 | .command("start") 100 | .assert() 101 | .code(1) 102 | .stderr(contains(format!( 103 | "{} Command is missing script or lang code which determines which executor to use.", 104 | "ERROR:".red() 105 | ))) 106 | .failure(); 107 | } 108 | } 109 | 110 | mod when_subcommands_do_not_include_their_parent_command_prefix { 111 | use super::*; 112 | 113 | #[test] 114 | fn subcommand_works() { 115 | let (_temp, maskfile_path) = common::maskfile( 116 | r#" 117 | ## services 118 | ### start 119 | #### all 120 | ~~~bash 121 | echo "Start all services" 122 | ~~~ 123 | 124 | ~~~powershell 125 | Write-Output "Start all services" 126 | ~~~ 127 | "#, 128 | ); 129 | 130 | common::run_mask(&maskfile_path) 131 | .cli("services start all") 132 | .assert() 133 | .stdout(contains("Start all services")) 134 | .success(); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at git@jakedeichert.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /maskfile.md: -------------------------------------------------------------------------------- 1 | # Tasks 2 | 3 | Development tasks for mask. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | ## run (maskfile_command) 14 | > Build and run mask in development mode 15 | 16 | **NOTE:** This uses `cargo run` to build and run `mask` in development mode. You must have a `maskfile` in the current directory (this file) and must supply a valid command for that `maskfile` (`maskfile_command`) in order to test the changes you've made to `mask`. Since you can only test against this `maskfile` for now, you can add subcommands to the bottom and run against those instead of running one of the existing commands. 17 | 18 | **EXAMPLE:** `mask run "test -h"` - outputs the help info of this `test` command 19 | 20 | **OPTIONS** 21 | * watch 22 | * flags: -w --watch 23 | * desc: Rebuild on file change 24 | 25 | ~~~bash 26 | if [[ $watch == "true" ]]; then 27 | watchexec --exts rs --restart "cargo run -- $maskfile_command" 28 | else 29 | cargo run -- $maskfile_command 30 | fi 31 | ~~~ 32 | 33 | **Note:** On Windows platforms, `mask` falls back to running `powershell` code blocks. 34 | 35 | ~~~powershell 36 | param ( 37 | $maskfile_command = $env:maskfile_command, 38 | $watch = $env:watch 39 | ) 40 | 41 | $cargo_cmd = "cargo run -- $maskfile_command" 42 | $extra_args = "--exts rs --restart $cargo_cmd" 43 | 44 | if ($watch) { 45 | Start-Process watchexec -ArgumentList $extra_args -NoNewWindow -PassThru 46 | } else { 47 | cargo run -- $maskfile_command 48 | } 49 | ~~~ 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | ## build 64 | > Build a release version of mask 65 | 66 | ~~~bash 67 | cargo build --release 68 | ~~~ 69 | 70 | ~~~powershell 71 | cargo build --release 72 | ~~~ 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | ## link 83 | > Build mask and replace your globally installed version with it for testing 84 | 85 | ~~~bash 86 | cargo install --force --path ./mask 87 | ~~~ 88 | 89 | ~~~powershell 90 | [Diagnostics.Process]::Start("cargo", "install --force --path ./mask").WaitForExit() 91 | ~~~ 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | ## test 103 | > Run all tests 104 | 105 | **OPTIONS** 106 | * file 107 | * flags: -f --file 108 | * type: string 109 | * desc: Only run tests from a specific filename 110 | 111 | ~~~bash 112 | extra_args="" 113 | 114 | if [[ "$verbose" == "true" ]]; then 115 | # Run tests linearly and make logs visible in output 116 | extra_args="-- --nocapture --test-threads=1" 117 | fi 118 | 119 | echo "Running tests..." 120 | if [[ -z "$file" ]]; then 121 | # Run all tests by default 122 | cargo test $extra_args 123 | else 124 | # Tests a specific integration filename 125 | cargo test --test $file $extra_args 126 | fi 127 | echo "Tests passed!" 128 | ~~~ 129 | 130 | ~~~powershell 131 | param ( 132 | $file = $env:file 133 | ) 134 | 135 | $extra_args = "" 136 | $verbose = $env:verbose 137 | 138 | if ($verbose) { 139 | $extra_args = "-- --nocapture --test-threads=1" 140 | } 141 | 142 | Write-Output "Running tests..." 143 | if (!$file) { 144 | cargo test $extra_args 145 | } else { 146 | cargo test --test $file $extra_args 147 | } 148 | Write-Output "Tests passed!" 149 | ~~~ 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | ## format 165 | > Format all source files 166 | 167 | **OPTIONS** 168 | * check 169 | * flags: -c --check 170 | * desc: Show which files are not formatted correctly 171 | 172 | ~~~bash 173 | if [[ $check == "true" ]]; then 174 | cargo fmt --all -- --check 175 | else 176 | cargo fmt 177 | fi 178 | ~~~ 179 | 180 | ~~~powershell 181 | param ( 182 | $check = $env:check 183 | ) 184 | 185 | if ($check) { 186 | cargo fmt --all -- --check 187 | } else { 188 | cargo fmt 189 | } 190 | ~~~ 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | ## lint 202 | > Lint the project with clippy 203 | 204 | ~~~bash 205 | cargo clippy 206 | ~~~ 207 | 208 | ~~~powershell 209 | cargo clippy 210 | ~~~ 211 | -------------------------------------------------------------------------------- /.github/workflows/create-release.yml: -------------------------------------------------------------------------------- 1 | name: Create Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | crateName: 7 | description: 'Crate to Release' 8 | required: true 9 | default: 'mask' 10 | type: choice 11 | options: 12 | - mask 13 | - mask-parser 14 | releaseVersion: 15 | description: 'Version' 16 | required: true 17 | changelogUpdated: 18 | description: 'Is the CHANGELOG up to date?' 19 | required: true 20 | type: boolean 21 | 22 | permissions: 23 | contents: write 24 | 25 | env: 26 | VERSION: ${{ github.event.inputs.releaseVersion }} 27 | 28 | jobs: 29 | release-mask: 30 | if: ${{ github.event.inputs.crateName == 'mask' }} 31 | runs-on: ubuntu-latest 32 | steps: 33 | - uses: actions/checkout@v4 34 | with: 35 | # Must use a PAT to bypass the branch protection rule (allows pushing commits without requiring PRs) 36 | token: ${{ secrets.GH_PAT_TO_TRIGGER_RELEASE_WORKFLOW }} 37 | - name: Validate version number input 38 | run: | 39 | if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then 40 | echo "ERROR: invalid version number supplied '$VERSION'" 41 | exit 1 42 | fi 43 | - name: Verify CHANGELOG was updated 44 | run: | 45 | if [[ "${{ github.event.inputs.changelogUpdated }}" != "true" ]]; then 46 | echo "ERROR: you must update CHANGELOG before creating a new release" 47 | exit 1 48 | fi 49 | 50 | UNRELEASED_CHANGES=$(sed -n '/## UNRELEASED/,/## v/{//b;p}' CHANGELOG.md) 51 | if [[ "$UNRELEASED_CHANGES" == "" ]]; then 52 | echo "ERROR: CHANGELOG is missing release notes" 53 | exit 1 54 | fi 55 | # Write the release notes to a temp file we'll use below 56 | echo "$UNRELEASED_CHANGES" > ../RELEASE_NOTES.txt 57 | - name: Set up git user 58 | run: | 59 | git config user.name github-actions 60 | git config user.email github-actions@github.com 61 | - name: Commit version bumps 62 | run: | 63 | # Bump the version in the changelog 64 | sed -i "s/## UNRELEASED/## UNRELEASED\\n\\n\\n## v$VERSION ($(date '+%Y-%m-%d'))/" "CHANGELOG.md" 65 | # Bump the crate version 66 | sed -i "3s/.*/version = \"$VERSION\"/" "mask/Cargo.toml" 67 | # Let cargo bump the version in the lockfile 68 | cargo check 69 | git add -A && git commit -m "Publish mask v$VERSION" 70 | git push 71 | - name: Create a new Release 72 | env: 73 | # Must use a PAT to ensure the Release workflow is triggered 74 | # https://docs.github.com/en/actions/using-workflows/triggering-a-workflow#triggering-a-workflow-from-a-workflow 75 | GH_TOKEN: ${{ secrets.GH_PAT_TO_TRIGGER_RELEASE_WORKFLOW }} 76 | run: | 77 | gh release create "mask/$VERSION" --title "mask v$VERSION" --notes-file ../RELEASE_NOTES.txt 78 | 79 | release-mask-parser: 80 | if: ${{ github.event.inputs.crateName == 'mask-parser' }} 81 | runs-on: ubuntu-latest 82 | steps: 83 | - uses: actions/checkout@v4 84 | with: 85 | # Must use a PAT to bypass the branch protection rule (allows pushing commits without requiring PRs) 86 | token: ${{ secrets.GH_PAT_TO_TRIGGER_RELEASE_WORKFLOW }} 87 | - name: Validate version number input 88 | run: | 89 | if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then 90 | echo "ERROR: invalid version number supplied '$VERSION'" 91 | exit 1 92 | fi 93 | - name: Set up git user 94 | run: | 95 | git config user.name github-actions 96 | git config user.email github-actions@github.com 97 | - name: Commit version bumps 98 | run: | 99 | # Bump the crate version 100 | sed -i "3s/.*/version = \"$VERSION\"/" "mask-parser/Cargo.toml" 101 | # Let cargo bump the version in the lockfile 102 | cargo check 103 | git add -A && git commit -m "Publish mask-parser v$VERSION" 104 | git push 105 | - name: Create a new Release 106 | env: 107 | # Must use a PAT to ensure the Release workflow is triggered 108 | # https://docs.github.com/en/actions/using-workflows/triggering-a-workflow#triggering-a-workflow-from-a-workflow 109 | GH_TOKEN: ${{ secrets.GH_PAT_TO_TRIGGER_RELEASE_WORKFLOW }} 110 | run: | 111 | gh release create "mask-parser/$VERSION" --title "mask-parser v$VERSION" 112 | -------------------------------------------------------------------------------- /mask-parser/src/maskfile.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | use serde_json::Value; 3 | 4 | #[derive(Debug, Serialize, Clone)] 5 | pub struct Maskfile { 6 | pub title: String, 7 | pub description: String, 8 | pub commands: Vec, 9 | } 10 | 11 | impl Maskfile { 12 | pub fn to_json(&self) -> Result { 13 | serde_json::to_value(&self) 14 | } 15 | } 16 | 17 | #[derive(Debug, Serialize, Clone)] 18 | pub struct Command { 19 | pub level: u8, 20 | pub name: String, 21 | pub description: String, 22 | pub script: Option