diff --git a/.github/workflows/check-python.yaml b/.github/workflows/check-python.yaml index 1bce1233..eeabb2a1 100644 --- a/.github/workflows/check-python.yaml +++ b/.github/workflows/check-python.yaml @@ -14,7 +14,10 @@ jobs: run: pipx install poetry - name: Install algokit - run: pipx install algokit + run: pipx install algokit + + - name: Run algokit localnet + run: algokit localnet start - name: Set up Python 3.12 uses: actions/setup-python@v5 diff --git a/.gitignore b/.gitignore index fe232bdb..b0da2ea8 100644 --- a/.gitignore +++ b/.gitignore @@ -176,4 +176,7 @@ examples/**/poetry.lock # playground folder for previewing templates .playground/* -!.playground/.gitkeep + +# Misc +examples/**/smart_contracts/artifacts +examples/**/.algokit/sources diff --git a/.playground/.gitkeep b/.playground/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/.vscode/tasks.json b/.vscode/tasks.json index a93c48cb..45c8a640 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -8,7 +8,7 @@ "-v", "init", "--name", - "test_output", + ".playground", "--no-git", "--defaults", "--UNSAFE-SECURITY-accept-template-url", @@ -23,7 +23,7 @@ "kind": "build", "isDefault": true }, - "dependsOn": ["Delete test_output folder"], + "dependsOn": ["Cleanup .playground folder"], "problemMatcher": [] }, { @@ -33,7 +33,7 @@ "-v", "init", "--name", - "test_output", + ".playground", "--no-git", "--defaults", "--UNSAFE-SECURITY-accept-template-url", @@ -51,7 +51,7 @@ "kind": "build", "isDefault": true }, - "dependsOn": ["Delete test_output folder"], + "dependsOn": ["Cleanup .playground folder"], "problemMatcher": [] }, { @@ -61,7 +61,7 @@ "-v", "init", "--name", - "test_output", + ".playground", "--no-git", "--UNSAFE-SECURITY-accept-template-url", "--template-url", @@ -71,16 +71,13 @@ "--no-bootstrap" ], "type": "shell", - "dependsOn": ["Delete test_output folder"], + "dependsOn": ["Cleanup .playground folder"], "problemMatcher": [] }, { - "label": "Delete test_output folder", + "label": "Cleanup .playground folder", "command": "rm", - "args": [ - "-rf", - "test_output" - ], + "args": ["-rf", ".playground"], "type": "shell", "windows": { "command": "./.vscode/clear.ps1" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0dd5485d..233da7df 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,23 +5,38 @@ This repository is a template for creating new AlgoKit projects. It includes a b ## Pre-requisites `poetry install` - Install the dependencies for the project. +`pipx install algokit` - Ensure cli is installed. ## Testing +Ensure localnet is running by executing `algokit localnet reset`. + ```bash -poetry run pytest +poetry run pytest -n auto ``` This will regenerate the tests for default `starter` and `production` presets as well as default tests for `generators` available on the template. ## Development +### Manual + ```bash -poetry run copier copy . .playground/{some_dummy_folder_name} --vcs-ref=HEAD --trust +poetry run copier copy . .playground --vcs-ref=HEAD --trust ``` To generate a dummy project into the `.playground` folder. This is useful for testing the template to quickly preview the output of the template before testing via `pytest`. +### Using VSCode Tasks + +In VSCode IDE, you can find the tasks in the `.vscode/tasks.json` file. To run them: + +1. Open the command palette (`Cmd+Shift+P` on macOS, `Ctrl+Shift+P` on Windows/Linux) and type `> Run Task` +2. Select the task you want to run +3. It will be generated for you under the .playground folder + +To cleanup the .playground folder run dedicated cleanup task. + ## Contributing ### Commits diff --git a/copier.yaml b/copier.yaml index 009763ee..683c8106 100644 --- a/copier.yaml +++ b/copier.yaml @@ -1,32 +1,39 @@ _subdirectory: template_content _templates_suffix: ".jinja" +use_workspace: + type: bool + when: false # never prompted to user explicitly, instead expect cli to auto fill (supported cli versions > v1.13.x) + help: Automatically filled by AlgoKit CLI (>1.13.x) - passes the --workspace/--no-workspace flag's value, can be used to reason whether this template is currently being instantiated as part of a workspace or not. + default: no + # questions -# project_name should never get prompted, AlgoKit should always pass it by convention project_name: type: str help: Name for this project. placeholder: "algorand-app" -contract_name: - type: str - help: Name of the default smart contract app. - placeholder: "hello_world" - default: "hello_world" - validator: >- - {% if not (contract_name | regex_search('^[a-z]+(?:_[a-z]+)*$')) %} - contract_name must be formatted in snake case. - {% endif %} - author_name: type: str help: Package author name placeholder: "Your Name" + default: "Your Name" author_email: type: str help: Package author email placeholder: "your@email.tld" + default: "your@email.tld" + +contract_name: + type: str + help: Name of the default smart contract app. + placeholder: "hello_world" + default: "hello_world" + validator: >- + {% if not (contract_name | regex_search('^[a-z]+(?:_[a-z]+)*$')) %} + contract_name must be formatted in snake case. + {% endif %} preset_name: type: str diff --git a/examples/generators/production_beaker_smart_contract_python/.algokit.toml b/examples/generators/production_beaker_smart_contract_python/.algokit.toml index b706fdf1..396a48a0 100644 --- a/examples/generators/production_beaker_smart_contract_python/.algokit.toml +++ b/examples/generators/production_beaker_smart_contract_python/.algokit.toml @@ -1,20 +1,49 @@ [algokit] -min_version = "v1.10.0" +min_version = "v2.0.0" -[deploy] +[generate.smart_contract] +description = "Adds new smart contract to existing project" +path = ".algokit/generators/create_contract" + +[project] +type = 'contract' +name = 'production_beaker_smart_contract_python' +artifacts = 'smart_contracts/artifacts' + +[project.deploy] command = "poetry run python -m smart_contracts deploy" environment_secrets = [ "DEPLOYER_MNEMONIC", "DISPENSER_MNEMONIC", ] -[deploy.localnet] +[project.deploy.localnet] environment_secrets = [] -[generate.smart_contract] -description = "Adds new smart contract to existing project" -path = ".algokit/generators/create_contract" +[project.run] +# Commands intented for use locally and in CI +build = { commands = [ + 'poetry run python -m smart_contracts build', +], description = 'Build all smart contracts in the project' } +test = { commands = [ + 'poetry run pytest', +], description = 'Run smart contract tests' } +audit = { commands = [ + 'poetry export --without=dev -o requirements.txt', + 'poetry run pip-audit -r requirements.txt', +], description = 'Audit with pip-audit' } +lint = { commands = [ + 'poetry run black --check .', + 'poetry run ruff .', + 'poetry run mypy', +], description = 'Perform linting' } +audit-teal = { commands = [ + # 🚨 IMPORTANT 🚨: For strict TEAL validation, remove --exclude statements. The default starter contract is not for production. Ensure thorough testing and adherence to best practices in smart contract development. This is not a replacement for a professional audit. + 'algokit task analyze smart_contracts/artifacts --recursive --force --exclude rekey-to --exclude is-updatable --exclude missing-fee-check --exclude is-deletable --exclude can-close-asset --exclude can-close-account --exclude unprotected-deletable --exclude unprotected-updatable', +], description = 'Audit TEAL files' } -[project] -type = 'contract' -name = 'production_beaker_smart_contract_python' +# Commands indented for CI only, prefixed with `ci-` by convention +ci-teal-diff = { commands = [ + 'git add -N ./smart_contracts/artifacts', + 'git diff --exit-code --minimal ./smart_contracts/artifacts', +], description = 'Check TEAL files for differences' } diff --git a/examples/generators/production_beaker_smart_contract_python/.algokit/generators/create_contract/smart_contracts/{{ contract_name }}/deploy_config.py.j2 b/examples/generators/production_beaker_smart_contract_python/.algokit/generators/create_contract/smart_contracts/{{ contract_name }}/deploy_config.py.j2 index bb169ac2..76a5a6c1 100644 --- a/examples/generators/production_beaker_smart_contract_python/.algokit/generators/create_contract/smart_contracts/{{ contract_name }}/deploy_config.py.j2 +++ b/examples/generators/production_beaker_smart_contract_python/.algokit/generators/create_contract/smart_contracts/{{ contract_name }}/deploy_config.py.j2 @@ -38,9 +38,11 @@ def deploy( if is_mainnet else algokit_utils.OnSchemaBreak.ReplaceApp ), - on_update=algokit_utils.OnUpdate.AppendApp - if is_mainnet - else algokit_utils.OnUpdate.UpdateApp, + on_update=( + algokit_utils.OnUpdate.AppendApp + if is_mainnet + else algokit_utils.OnUpdate.UpdateApp + ), allow_delete=not is_mainnet, allow_update=not is_mainnet, ) diff --git a/examples/generators/production_beaker_smart_contract_python/.github/workflows/checks.yaml b/examples/generators/production_beaker_smart_contract_python/.github/workflows/checks.yaml deleted file mode 100644 index 4825470b..00000000 --- a/examples/generators/production_beaker_smart_contract_python/.github/workflows/checks.yaml +++ /dev/null @@ -1,82 +0,0 @@ -name: Check code base - -on: - workflow_call: - -jobs: - checks: - runs-on: 'ubuntu-latest' - steps: - - name: Checkout source code - uses: actions/checkout@v4 - - - name: Install poetry - run: pipx install poetry - - - name: Set up Python 3.12 - uses: actions/setup-python@v5 - with: - python-version: '3.12' - cache: 'poetry' - - - name: Install algokit - run: pipx install algokit - - - name: Start LocalNet - run: algokit localnet start - - - name: Bootstrap dependencies - run: algokit bootstrap all - - - name: Configure git - shell: bash - run: | - # set git user and email as test invoke git - git config --global user.email "actions@github.com" && git config --global user.name "github-actions" - - - name: Audit with pip-audit - run: | - # audit non dev dependencies, no exclusions - poetry export --without=dev > requirements.txt && poetry run pip-audit -r requirements.txt - - # audit all dependencies, with exclusions. - # If a vulnerability is found in a dev dependency without an available fix, - # it can be temporarily ignored by adding --ignore-vuln e.g. - # --ignore-vuln "GHSA-hcpj-qp55-gfph" # GitPython vulnerability, dev only dependency - poetry run pip-audit - - - name: Check formatting with Black - run: | - # stop the build if there are files that don't meet formatting requirements - poetry run black --check . - - - name: Check linting with Ruff - run: | - # stop the build if there are Python syntax errors or undefined names - poetry run ruff . - - - name: Build smart contracts - run: poetry run python -m smart_contracts build - - - name: Check types with mypy - run: poetry run mypy - - - name: Run tests - shell: bash - run: | - set -o pipefail - poetry run pytest --junitxml=pytest-junit.xml - - - name: Scan TEAL files for issues - run: algokit task analyze .algokit --recursive --force # add --diff flag if you want output stability checks instead - - - name: Check output stability of the smart contracts - shell: bash - run: | - # Add untracked files as empty so they come up in diff - git add -N ./smart_contracts/artifacts - # Error out if there are any changes in teal after generating output - git diff --exit-code --minimal ./smart_contracts/artifacts || (echo "::error ::Smart contract artifacts have changed, ensure committed artifacts are up to date" && exit 1); - - - name: Run deployer against LocalNet - run: poetry run python -m smart_contracts deploy diff --git a/examples/generators/production_beaker_smart_contract_python/.github/workflows/pr.yaml b/examples/generators/production_beaker_smart_contract_python/.github/workflows/pr.yaml deleted file mode 100644 index a80f7840..00000000 --- a/examples/generators/production_beaker_smart_contract_python/.github/workflows/pr.yaml +++ /dev/null @@ -1,8 +0,0 @@ -name: Pull Request validation - -on: [pull_request] - -jobs: - pr-check: - name: Perform Checks - uses: ./.github/workflows/checks.yaml diff --git a/examples/generators/production_beaker_smart_contract_python/.github/workflows/cd.yaml b/examples/generators/production_beaker_smart_contract_python/.github/workflows/production-beaker-smart-contract-python-cd.yaml similarity index 63% rename from examples/generators/production_beaker_smart_contract_python/.github/workflows/cd.yaml rename to examples/generators/production_beaker_smart_contract_python/.github/workflows/production-beaker-smart-contract-python-cd.yaml index f4a5dc7f..91866917 100644 --- a/examples/generators/production_beaker_smart_contract_python/.github/workflows/cd.yaml +++ b/examples/generators/production_beaker_smart_contract_python/.github/workflows/production-beaker-smart-contract-python-cd.yaml @@ -1,21 +1,19 @@ -name: Continuous Delivery of Smart Contract +name: Release production_beaker_smart_contract_python on: + workflow_call: push: branches: - main -concurrency: release - jobs: - ci-check: - name: Perform Checks - uses: ./.github/workflows/checks.yaml - + validate: + name: Validate production_beaker_smart_contract_python + uses: ./.github/workflows/production-beaker-smart-contract-python-ci.yaml deploy-testnet: - runs-on: 'ubuntu-latest' - needs: ci-check - environment: Test + runs-on: "ubuntu-latest" + needs: validate + environment: contract-testnet steps: - name: Checkout source code uses: actions/checkout@v4 @@ -26,14 +24,14 @@ jobs: - name: Set up Python 3.12 uses: actions/setup-python@v5 with: - python-version: '3.12' - cache: 'poetry' + python-version: "3.12" + cache: "poetry" - name: Install algokit run: pipx install algokit - name: Bootstrap dependencies - run: algokit bootstrap all + run: algokit bootstrap all --project-name 'production_beaker_smart_contract_python' - name: Configure git shell: bash @@ -42,7 +40,7 @@ jobs: git config --global user.email "actions@github.com" && git config --global user.name "github-actions" - name: Deploy to testnet - run: algokit deploy testnet + run: algokit deploy testnet --project-name 'production_beaker_smart_contract_python' env: # This is the account that becomes the creator of the contract DEPLOYER_MNEMONIC: ${{ secrets.DEPLOYER_MNEMONIC }} diff --git a/examples/generators/production_beaker_smart_contract_python/.github/workflows/production-beaker-smart-contract-python-ci.yaml b/examples/generators/production_beaker_smart_contract_python/.github/workflows/production-beaker-smart-contract-python-ci.yaml new file mode 100644 index 00000000..671d1c22 --- /dev/null +++ b/examples/generators/production_beaker_smart_contract_python/.github/workflows/production-beaker-smart-contract-python-ci.yaml @@ -0,0 +1,60 @@ +name: Validate production_beaker_smart_contract_python + +on: + workflow_call: + pull_request: + +jobs: + validate: + runs-on: "ubuntu-latest" + steps: + - name: Checkout source code + uses: actions/checkout@v4 + + - name: Install poetry + run: pipx install poetry + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: "poetry" + + - name: Install algokit + run: pipx install algokit + + - name: Start LocalNet + run: algokit localnet start + + - name: Bootstrap dependencies + run: algokit bootstrap all --project-name 'production_beaker_smart_contract_python' + + - name: Configure git + shell: bash + run: | + # set git user and email as test invoke git + git config --global user.email "actions@github.com" && git config --global user.name "github-actions" + + - name: Audit python dependencies + run: algokit project run audit --project-name 'production_beaker_smart_contract_python' + + - name: Lint and format python dependencies + run: algokit project run lint --project-name 'production_beaker_smart_contract_python' + + - name: Run tests + shell: bash + run: | + set -o pipefail + algokit project run test --project-name 'production_beaker_smart_contract_python' + + - name: Build smart contracts + run: algokit project run build --project-name 'production_beaker_smart_contract_python' + + - name: Scan TEAL files for issues + run: algokit project run audit-teal --project-name 'production_beaker_smart_contract_python' + + - name: Check output stability of the smart contracts + run: algokit project run ci-teal-diff --project-name 'production_beaker_smart_contract_python' + + - name: Run deployer against LocalNet + run: algokit project deploy localnet --project-name 'production_beaker_smart_contract_python' diff --git a/examples/generators/production_beaker_smart_contract_python/README.md b/examples/generators/production_beaker_smart_contract_python/README.md index 6b98ef75..2ef55870 100644 --- a/examples/generators/production_beaker_smart_contract_python/README.md +++ b/examples/generators/production_beaker_smart_contract_python/README.md @@ -43,7 +43,9 @@ For an interactive guided walkthrough of the project install [CodeTour](https:// > For guidance on `smart_contracts` folder and adding new contracts to the project please see [README](smart_contracts/README.md) on the respective folder.### Continuous Integration / Continuous Deployment (CI/CD) -This project uses [GitHub Actions](https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions) to define CI/CD workflows, which are located in the [`.github/workflows`](./.github/workflows) folder. +This project uses [GitHub Actions](https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions) to define CI/CD workflows, which are located in the [.github/workflows](`.github/workflows`) folder. + +> Please note, if you instantiated the project with --workspace flag in `algokit init` it will automatically attempt to move the contents of the `.github` folder to the root of the workspace. ### Debugging Smart Contracts diff --git a/examples/generators/production_beaker_smart_contract_python/smart_contracts/config.py b/examples/generators/production_beaker_smart_contract_python/smart_contracts/config.py index 3a1d85fe..89d948da 100644 --- a/examples/generators/production_beaker_smart_contract_python/smart_contracts/config.py +++ b/examples/generators/production_beaker_smart_contract_python/smart_contracts/config.py @@ -12,9 +12,10 @@ @dataclasses.dataclass class SmartContract: app: Application - deploy: Callable[ - [AlgodClient, IndexerClient, ApplicationSpecification, Account], None - ] | None = None + deploy: ( + Callable[[AlgodClient, IndexerClient, ApplicationSpecification, Account], None] + | None + ) = None def import_contract(folder: Path) -> Application: diff --git a/examples/generators/production_beaker_smart_contract_python/smart_contracts/cool_contract/deploy_config.py b/examples/generators/production_beaker_smart_contract_python/smart_contracts/cool_contract/deploy_config.py index 964de9a0..5e819f52 100644 --- a/examples/generators/production_beaker_smart_contract_python/smart_contracts/cool_contract/deploy_config.py +++ b/examples/generators/production_beaker_smart_contract_python/smart_contracts/cool_contract/deploy_config.py @@ -30,9 +30,11 @@ def deploy( if is_mainnet else algokit_utils.OnSchemaBreak.ReplaceApp ), - on_update=algokit_utils.OnUpdate.AppendApp - if is_mainnet - else algokit_utils.OnUpdate.UpdateApp, + on_update=( + algokit_utils.OnUpdate.AppendApp + if is_mainnet + else algokit_utils.OnUpdate.UpdateApp + ), allow_delete=not is_mainnet, allow_update=not is_mainnet, ) diff --git a/examples/generators/production_beaker_smart_contract_python/smart_contracts/hello_world/deploy_config.py b/examples/generators/production_beaker_smart_contract_python/smart_contracts/hello_world/deploy_config.py index 7ed7ad6c..46eeb59e 100644 --- a/examples/generators/production_beaker_smart_contract_python/smart_contracts/hello_world/deploy_config.py +++ b/examples/generators/production_beaker_smart_contract_python/smart_contracts/hello_world/deploy_config.py @@ -30,9 +30,11 @@ def deploy( if is_mainnet else algokit_utils.OnSchemaBreak.ReplaceApp ), - on_update=algokit_utils.OnUpdate.AppendApp - if is_mainnet - else algokit_utils.OnUpdate.UpdateApp, + on_update=( + algokit_utils.OnUpdate.AppendApp + if is_mainnet + else algokit_utils.OnUpdate.UpdateApp + ), allow_delete=not is_mainnet, allow_update=not is_mainnet, ) diff --git a/examples/generators/production_beaker_smart_contract_typescript/.algokit.toml b/examples/generators/production_beaker_smart_contract_typescript/.algokit.toml index 4d2eae49..b8545aed 100644 --- a/examples/generators/production_beaker_smart_contract_typescript/.algokit.toml +++ b/examples/generators/production_beaker_smart_contract_typescript/.algokit.toml @@ -1,20 +1,49 @@ [algokit] -min_version = "v1.10.0" +min_version = "v2.0.0" -[deploy] +[generate.smart_contract] +description = "Adds new smart contract to existing project" +path = ".algokit/generators/create_contract" + +[project] +type = 'contract' +name = 'production_beaker_smart_contract_typescript' +artifacts = 'smart_contracts/artifacts' + +[project.deploy] command = "npm run deploy:ci" environment_secrets = [ "DEPLOYER_MNEMONIC", "DISPENSER_MNEMONIC", ] -[deploy.localnet] +[project.deploy.localnet] environment_secrets = [] -[generate.smart_contract] -description = "Adds new smart contract to existing project" -path = ".algokit/generators/create_contract" +[project.run] +# Commands intented for use locally and in CI +build = { commands = [ + 'poetry run python -m smart_contracts build', +], description = 'Build all smart contracts in the project' } +test = { commands = [ + 'npm run test', +], description = 'Run smart contract tests using Jest' } +audit = { commands = [ + 'poetry export --without=dev -o requirements.txt', + 'poetry run pip-audit -r requirements.txt', +], description = 'Audit with pip-audit' } +lint = { commands = [ + 'poetry run black --check .', + 'poetry run ruff .', + 'poetry run mypy', +], description = 'Perform linting' } +audit-teal = { commands = [ + # 🚨 IMPORTANT 🚨: For strict TEAL validation, remove --exclude statements. The default starter contract is not for production. Ensure thorough testing and adherence to best practices in smart contract development. This is not a replacement for a professional audit. + 'algokit task analyze smart_contracts/artifacts --recursive --force --exclude rekey-to --exclude is-updatable --exclude missing-fee-check --exclude is-deletable --exclude can-close-asset --exclude can-close-account --exclude unprotected-deletable --exclude unprotected-updatable', +], description = 'Audit TEAL files' } -[project] -type = 'contract' -name = 'production_beaker_smart_contract_typescript' +# Commands indented for CI only, prefixed with `ci-` by convention +ci-teal-diff = { commands = [ + 'git add -N ./smart_contracts/artifacts', + 'git diff --exit-code --minimal ./smart_contracts/artifacts', +], description = 'Check TEAL files for differences' } diff --git a/examples/generators/production_beaker_smart_contract_typescript/.github/workflows/checks.yaml b/examples/generators/production_beaker_smart_contract_typescript/.github/workflows/checks.yaml deleted file mode 100644 index 46af22d2..00000000 --- a/examples/generators/production_beaker_smart_contract_typescript/.github/workflows/checks.yaml +++ /dev/null @@ -1,80 +0,0 @@ -name: Check code base - -on: - workflow_call: - -jobs: - checks: - runs-on: 'ubuntu-latest' - steps: - - name: Checkout source code - uses: actions/checkout@v4 - - - name: Install poetry - run: pipx install poetry - - - name: Set up Python 3.12 - uses: actions/setup-python@v5 - with: - python-version: '3.12' - cache: 'poetry' - - - name: Install algokit - run: pipx install algokit - - - name: Start LocalNet - run: algokit localnet start - - - name: Bootstrap dependencies - run: algokit bootstrap all - - - name: Configure git - shell: bash - run: | - # set git user and email as test invoke git - git config --global user.email "actions@github.com" && git config --global user.name "github-actions" - - - name: Audit with pip-audit - run: | - # audit non dev dependencies, no exclusions - poetry export --without=dev > requirements.txt && poetry run pip-audit -r requirements.txt - - # audit all dependencies, with exclusions. - # If a vulnerability is found in a dev dependency without an available fix, - # it can be temporarily ignored by adding --ignore-vuln e.g. - # --ignore-vuln "GHSA-hcpj-qp55-gfph" # GitPython vulnerability, dev only dependency - poetry run pip-audit - - - name: Check formatting with Black - run: | - # stop the build if there are files that don't meet formatting requirements - poetry run black --check . - - - name: Check linting with Ruff - run: | - # stop the build if there are Python syntax errors or undefined names - poetry run ruff . - - - name: Build smart contracts - run: poetry run python -m smart_contracts build - - - name: Check types with mypy - run: poetry run mypy - - - name: Run tests - shell: bash - run: npm run test - - - name: Scan TEAL files for issues - run: algokit task analyze .algokit --recursive --force # add --diff flag if you want output stability checks instead - - - name: Check output stability of the smart contracts - shell: bash - run: | - # Add untracked files as empty so they come up in diff - git add -N ./smart_contracts/artifacts - # Error out if there are any changes in teal after generating output - git diff --exit-code --minimal ./smart_contracts/artifacts || (echo "::error ::Smart contract artifacts have changed, ensure committed artifacts are up to date" && exit 1); - - - name: Run deployer against LocalNet - run: npm run deploy:ci diff --git a/examples/generators/production_beaker_smart_contract_typescript/.github/workflows/pr.yaml b/examples/generators/production_beaker_smart_contract_typescript/.github/workflows/pr.yaml deleted file mode 100644 index a80f7840..00000000 --- a/examples/generators/production_beaker_smart_contract_typescript/.github/workflows/pr.yaml +++ /dev/null @@ -1,8 +0,0 @@ -name: Pull Request validation - -on: [pull_request] - -jobs: - pr-check: - name: Perform Checks - uses: ./.github/workflows/checks.yaml diff --git a/examples/production_beaker/.github/workflows/cd.yaml b/examples/generators/production_beaker_smart_contract_typescript/.github/workflows/production-beaker-smart-contract-typescript-cd.yaml similarity index 62% rename from examples/production_beaker/.github/workflows/cd.yaml rename to examples/generators/production_beaker_smart_contract_typescript/.github/workflows/production-beaker-smart-contract-typescript-cd.yaml index f4a5dc7f..ecd2db48 100644 --- a/examples/production_beaker/.github/workflows/cd.yaml +++ b/examples/generators/production_beaker_smart_contract_typescript/.github/workflows/production-beaker-smart-contract-typescript-cd.yaml @@ -1,21 +1,19 @@ -name: Continuous Delivery of Smart Contract +name: Release production_beaker_smart_contract_typescript on: + workflow_call: push: branches: - main -concurrency: release - jobs: - ci-check: - name: Perform Checks - uses: ./.github/workflows/checks.yaml - + validate: + name: Validate production_beaker_smart_contract_typescript + uses: ./.github/workflows/production-beaker-smart-contract-typescript-ci.yaml deploy-testnet: - runs-on: 'ubuntu-latest' - needs: ci-check - environment: Test + runs-on: "ubuntu-latest" + needs: validate + environment: contract-testnet steps: - name: Checkout source code uses: actions/checkout@v4 @@ -26,14 +24,14 @@ jobs: - name: Set up Python 3.12 uses: actions/setup-python@v5 with: - python-version: '3.12' - cache: 'poetry' + python-version: "3.12" + cache: "poetry" - name: Install algokit run: pipx install algokit - name: Bootstrap dependencies - run: algokit bootstrap all + run: algokit bootstrap all --project-name 'production_beaker_smart_contract_typescript' - name: Configure git shell: bash @@ -42,7 +40,7 @@ jobs: git config --global user.email "actions@github.com" && git config --global user.name "github-actions" - name: Deploy to testnet - run: algokit deploy testnet + run: algokit deploy testnet --project-name 'production_beaker_smart_contract_typescript' env: # This is the account that becomes the creator of the contract DEPLOYER_MNEMONIC: ${{ secrets.DEPLOYER_MNEMONIC }} diff --git a/examples/generators/production_beaker_smart_contract_typescript/.github/workflows/production-beaker-smart-contract-typescript-ci.yaml b/examples/generators/production_beaker_smart_contract_typescript/.github/workflows/production-beaker-smart-contract-typescript-ci.yaml new file mode 100644 index 00000000..6763a518 --- /dev/null +++ b/examples/generators/production_beaker_smart_contract_typescript/.github/workflows/production-beaker-smart-contract-typescript-ci.yaml @@ -0,0 +1,60 @@ +name: Validate production_beaker_smart_contract_typescript + +on: + workflow_call: + pull_request: + +jobs: + validate: + runs-on: "ubuntu-latest" + steps: + - name: Checkout source code + uses: actions/checkout@v4 + + - name: Install poetry + run: pipx install poetry + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: "poetry" + + - name: Install algokit + run: pipx install algokit + + - name: Start LocalNet + run: algokit localnet start + + - name: Bootstrap dependencies + run: algokit bootstrap all --project-name 'production_beaker_smart_contract_typescript' + + - name: Configure git + shell: bash + run: | + # set git user and email as test invoke git + git config --global user.email "actions@github.com" && git config --global user.name "github-actions" + + - name: Audit python dependencies + run: algokit project run audit --project-name 'production_beaker_smart_contract_typescript' + + - name: Lint and format python dependencies + run: algokit project run lint --project-name 'production_beaker_smart_contract_typescript' + + - name: Run tests + shell: bash + run: | + set -o pipefail + algokit project run test --project-name 'production_beaker_smart_contract_typescript' + + - name: Build smart contracts + run: algokit project run build --project-name 'production_beaker_smart_contract_typescript' + + - name: Scan TEAL files for issues + run: algokit project run audit-teal --project-name 'production_beaker_smart_contract_typescript' + + - name: Check output stability of the smart contracts + run: algokit project run ci-teal-diff --project-name 'production_beaker_smart_contract_typescript' + + - name: Run deployer against LocalNet + run: algokit project deploy localnet --project-name 'production_beaker_smart_contract_typescript' diff --git a/examples/generators/production_beaker_smart_contract_typescript/README.md b/examples/generators/production_beaker_smart_contract_typescript/README.md index 51bcc9f8..9b60de28 100644 --- a/examples/generators/production_beaker_smart_contract_typescript/README.md +++ b/examples/generators/production_beaker_smart_contract_typescript/README.md @@ -44,7 +44,9 @@ For an interactive guided walkthrough of the project install [CodeTour](https:// > For guidance on `smart_contracts` folder and adding new contracts to the project please see [README](smart_contracts/README.md) on the respective folder.### Continuous Integration / Continuous Deployment (CI/CD) -This project uses [GitHub Actions](https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions) to define CI/CD workflows, which are located in the [`.github/workflows`](./.github/workflows) folder. +This project uses [GitHub Actions](https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions) to define CI/CD workflows, which are located in the [.github/workflows](`.github/workflows`) folder. + +> Please note, if you instantiated the project with --workspace flag in `algokit init` it will automatically attempt to move the contents of the `.github` folder to the root of the workspace. ### Debugging Smart Contracts diff --git a/examples/generators/production_beaker_smart_contract_typescript/smart_contracts/config.py b/examples/generators/production_beaker_smart_contract_typescript/smart_contracts/config.py index 3a1d85fe..89d948da 100644 --- a/examples/generators/production_beaker_smart_contract_typescript/smart_contracts/config.py +++ b/examples/generators/production_beaker_smart_contract_typescript/smart_contracts/config.py @@ -12,9 +12,10 @@ @dataclasses.dataclass class SmartContract: app: Application - deploy: Callable[ - [AlgodClient, IndexerClient, ApplicationSpecification, Account], None - ] | None = None + deploy: ( + Callable[[AlgodClient, IndexerClient, ApplicationSpecification, Account], None] + | None + ) = None def import_contract(folder: Path) -> Application: diff --git a/examples/generators/starter_beaker_smart_contract_python/.algokit.toml b/examples/generators/starter_beaker_smart_contract_python/.algokit.toml index 220f03c4..7d737d1c 100644 --- a/examples/generators/starter_beaker_smart_contract_python/.algokit.toml +++ b/examples/generators/starter_beaker_smart_contract_python/.algokit.toml @@ -1,19 +1,38 @@ [algokit] -min_version = "v1.10.0" +min_version = "v2.0.0" -[deploy] +[generate.smart_contract] +description = "Adds new smart contract to existing project" +path = ".algokit/generators/create_contract" + +[project] +type = 'contract' +name = 'starter_beaker_smart_contract_python' +artifacts = 'smart_contracts/artifacts' + +[project.deploy] command = "poetry run python -m smart_contracts deploy" environment_secrets = [ "DEPLOYER_MNEMONIC", ] -[deploy.localnet] +[project.deploy.localnet] environment_secrets = [] -[generate.smart_contract] -description = "Adds new smart contract to existing project" -path = ".algokit/generators/create_contract" +[project.run] +# Commands intented for use locally and in CI +build = { commands = [ + 'poetry run python -m smart_contracts build', +], description = 'Build all smart contracts in the project' } +lint = { commands = [ +], description = 'Perform linting' } +audit-teal = { commands = [ + # 🚨 IMPORTANT 🚨: For strict TEAL validation, remove --exclude statements. The default starter contract is not for production. Ensure thorough testing and adherence to best practices in smart contract development. This is not a replacement for a professional audit. + 'algokit task analyze smart_contracts/artifacts --recursive --force --exclude rekey-to --exclude is-updatable --exclude missing-fee-check --exclude is-deletable --exclude can-close-asset --exclude can-close-account --exclude unprotected-deletable --exclude unprotected-updatable', +], description = 'Audit TEAL files' } -[project] -type = 'contract' -name = 'starter_beaker_smart_contract_python' +# Commands indented for CI only, prefixed with `ci-` by convention +ci-teal-diff = { commands = [ + 'git add -N ./smart_contracts/artifacts', + 'git diff --exit-code --minimal ./smart_contracts/artifacts', +], description = 'Check TEAL files for differences' } diff --git a/examples/generators/starter_beaker_smart_contract_python/.algokit/generators/create_contract/smart_contracts/{{ contract_name }}/deploy_config.py.j2 b/examples/generators/starter_beaker_smart_contract_python/.algokit/generators/create_contract/smart_contracts/{{ contract_name }}/deploy_config.py.j2 index bb169ac2..76a5a6c1 100644 --- a/examples/generators/starter_beaker_smart_contract_python/.algokit/generators/create_contract/smart_contracts/{{ contract_name }}/deploy_config.py.j2 +++ b/examples/generators/starter_beaker_smart_contract_python/.algokit/generators/create_contract/smart_contracts/{{ contract_name }}/deploy_config.py.j2 @@ -38,9 +38,11 @@ def deploy( if is_mainnet else algokit_utils.OnSchemaBreak.ReplaceApp ), - on_update=algokit_utils.OnUpdate.AppendApp - if is_mainnet - else algokit_utils.OnUpdate.UpdateApp, + on_update=( + algokit_utils.OnUpdate.AppendApp + if is_mainnet + else algokit_utils.OnUpdate.UpdateApp + ), allow_delete=not is_mainnet, allow_update=not is_mainnet, ) diff --git a/examples/generators/starter_beaker_smart_contract_python/smart_contracts/config.py b/examples/generators/starter_beaker_smart_contract_python/smart_contracts/config.py index 3a1d85fe..89d948da 100644 --- a/examples/generators/starter_beaker_smart_contract_python/smart_contracts/config.py +++ b/examples/generators/starter_beaker_smart_contract_python/smart_contracts/config.py @@ -12,9 +12,10 @@ @dataclasses.dataclass class SmartContract: app: Application - deploy: Callable[ - [AlgodClient, IndexerClient, ApplicationSpecification, Account], None - ] | None = None + deploy: ( + Callable[[AlgodClient, IndexerClient, ApplicationSpecification, Account], None] + | None + ) = None def import_contract(folder: Path) -> Application: diff --git a/examples/generators/starter_beaker_smart_contract_typescript/.algokit.toml b/examples/generators/starter_beaker_smart_contract_typescript/.algokit.toml index 26079353..f6bfbdbd 100644 --- a/examples/generators/starter_beaker_smart_contract_typescript/.algokit.toml +++ b/examples/generators/starter_beaker_smart_contract_typescript/.algokit.toml @@ -1,19 +1,38 @@ [algokit] -min_version = "v1.10.0" +min_version = "v2.0.0" -[deploy] +[generate.smart_contract] +description = "Adds new smart contract to existing project" +path = ".algokit/generators/create_contract" + +[project] +type = 'contract' +name = 'starter_beaker_smart_contract_typescript' +artifacts = 'smart_contracts/artifacts' + +[project.deploy] command = "npm run deploy:ci" environment_secrets = [ "DEPLOYER_MNEMONIC", ] -[deploy.localnet] +[project.deploy.localnet] environment_secrets = [] -[generate.smart_contract] -description = "Adds new smart contract to existing project" -path = ".algokit/generators/create_contract" +[project.run] +# Commands intented for use locally and in CI +build = { commands = [ + 'poetry run python -m smart_contracts build', +], description = 'Build all smart contracts in the project' } +lint = { commands = [ +], description = 'Perform linting' } +audit-teal = { commands = [ + # 🚨 IMPORTANT 🚨: For strict TEAL validation, remove --exclude statements. The default starter contract is not for production. Ensure thorough testing and adherence to best practices in smart contract development. This is not a replacement for a professional audit. + 'algokit task analyze smart_contracts/artifacts --recursive --force --exclude rekey-to --exclude is-updatable --exclude missing-fee-check --exclude is-deletable --exclude can-close-asset --exclude can-close-account --exclude unprotected-deletable --exclude unprotected-updatable', +], description = 'Audit TEAL files' } -[project] -type = 'contract' -name = 'starter_beaker_smart_contract_typescript' +# Commands indented for CI only, prefixed with `ci-` by convention +ci-teal-diff = { commands = [ + 'git add -N ./smart_contracts/artifacts', + 'git diff --exit-code --minimal ./smart_contracts/artifacts', +], description = 'Check TEAL files for differences' } diff --git a/examples/generators/starter_beaker_smart_contract_typescript/smart_contracts/config.py b/examples/generators/starter_beaker_smart_contract_typescript/smart_contracts/config.py index 3a1d85fe..89d948da 100644 --- a/examples/generators/starter_beaker_smart_contract_typescript/smart_contracts/config.py +++ b/examples/generators/starter_beaker_smart_contract_typescript/smart_contracts/config.py @@ -12,9 +12,10 @@ @dataclasses.dataclass class SmartContract: app: Application - deploy: Callable[ - [AlgodClient, IndexerClient, ApplicationSpecification, Account], None - ] | None = None + deploy: ( + Callable[[AlgodClient, IndexerClient, ApplicationSpecification, Account], None] + | None + ) = None def import_contract(folder: Path) -> Application: diff --git a/examples/production_beaker/.algokit.toml b/examples/production_beaker/.algokit.toml index d6786166..a6a2bad7 100644 --- a/examples/production_beaker/.algokit.toml +++ b/examples/production_beaker/.algokit.toml @@ -1,20 +1,49 @@ [algokit] -min_version = "v1.10.0" +min_version = "v2.0.0" -[deploy] +[generate.smart_contract] +description = "Adds new smart contract to existing project" +path = ".algokit/generators/create_contract" + +[project] +type = 'contract' +name = 'production_beaker' +artifacts = 'smart_contracts/artifacts' + +[project.deploy] command = "poetry run python -m smart_contracts deploy" environment_secrets = [ "DEPLOYER_MNEMONIC", "DISPENSER_MNEMONIC", ] -[deploy.localnet] +[project.deploy.localnet] environment_secrets = [] -[generate.smart_contract] -description = "Adds new smart contract to existing project" -path = ".algokit/generators/create_contract" +[project.run] +# Commands intented for use locally and in CI +build = { commands = [ + 'poetry run python -m smart_contracts build', +], description = 'Build all smart contracts in the project' } +test = { commands = [ + 'poetry run pytest', +], description = 'Run smart contract tests' } +audit = { commands = [ + 'poetry export --without=dev -o requirements.txt', + 'poetry run pip-audit -r requirements.txt', +], description = 'Audit with pip-audit' } +lint = { commands = [ + 'poetry run black --check .', + 'poetry run ruff .', + 'poetry run mypy', +], description = 'Perform linting' } +audit-teal = { commands = [ + # 🚨 IMPORTANT 🚨: For strict TEAL validation, remove --exclude statements. The default starter contract is not for production. Ensure thorough testing and adherence to best practices in smart contract development. This is not a replacement for a professional audit. + 'algokit task analyze smart_contracts/artifacts --recursive --force --exclude rekey-to --exclude is-updatable --exclude missing-fee-check --exclude is-deletable --exclude can-close-asset --exclude can-close-account --exclude unprotected-deletable --exclude unprotected-updatable', +], description = 'Audit TEAL files' } -[project] -type = 'contract' -name = 'production_beaker' +# Commands indented for CI only, prefixed with `ci-` by convention +ci-teal-diff = { commands = [ + 'git add -N ./smart_contracts/artifacts', + 'git diff --exit-code --minimal ./smart_contracts/artifacts', +], description = 'Check TEAL files for differences' } diff --git a/examples/production_beaker/.github/workflows/checks.yaml b/examples/production_beaker/.github/workflows/checks.yaml deleted file mode 100644 index 4825470b..00000000 --- a/examples/production_beaker/.github/workflows/checks.yaml +++ /dev/null @@ -1,82 +0,0 @@ -name: Check code base - -on: - workflow_call: - -jobs: - checks: - runs-on: 'ubuntu-latest' - steps: - - name: Checkout source code - uses: actions/checkout@v4 - - - name: Install poetry - run: pipx install poetry - - - name: Set up Python 3.12 - uses: actions/setup-python@v5 - with: - python-version: '3.12' - cache: 'poetry' - - - name: Install algokit - run: pipx install algokit - - - name: Start LocalNet - run: algokit localnet start - - - name: Bootstrap dependencies - run: algokit bootstrap all - - - name: Configure git - shell: bash - run: | - # set git user and email as test invoke git - git config --global user.email "actions@github.com" && git config --global user.name "github-actions" - - - name: Audit with pip-audit - run: | - # audit non dev dependencies, no exclusions - poetry export --without=dev > requirements.txt && poetry run pip-audit -r requirements.txt - - # audit all dependencies, with exclusions. - # If a vulnerability is found in a dev dependency without an available fix, - # it can be temporarily ignored by adding --ignore-vuln e.g. - # --ignore-vuln "GHSA-hcpj-qp55-gfph" # GitPython vulnerability, dev only dependency - poetry run pip-audit - - - name: Check formatting with Black - run: | - # stop the build if there are files that don't meet formatting requirements - poetry run black --check . - - - name: Check linting with Ruff - run: | - # stop the build if there are Python syntax errors or undefined names - poetry run ruff . - - - name: Build smart contracts - run: poetry run python -m smart_contracts build - - - name: Check types with mypy - run: poetry run mypy - - - name: Run tests - shell: bash - run: | - set -o pipefail - poetry run pytest --junitxml=pytest-junit.xml - - - name: Scan TEAL files for issues - run: algokit task analyze .algokit --recursive --force # add --diff flag if you want output stability checks instead - - - name: Check output stability of the smart contracts - shell: bash - run: | - # Add untracked files as empty so they come up in diff - git add -N ./smart_contracts/artifacts - # Error out if there are any changes in teal after generating output - git diff --exit-code --minimal ./smart_contracts/artifacts || (echo "::error ::Smart contract artifacts have changed, ensure committed artifacts are up to date" && exit 1); - - - name: Run deployer against LocalNet - run: poetry run python -m smart_contracts deploy diff --git a/examples/production_beaker/.github/workflows/pr.yaml b/examples/production_beaker/.github/workflows/pr.yaml deleted file mode 100644 index a80f7840..00000000 --- a/examples/production_beaker/.github/workflows/pr.yaml +++ /dev/null @@ -1,8 +0,0 @@ -name: Pull Request validation - -on: [pull_request] - -jobs: - pr-check: - name: Perform Checks - uses: ./.github/workflows/checks.yaml diff --git a/examples/generators/production_beaker_smart_contract_typescript/.github/workflows/cd.yaml b/examples/production_beaker/.github/workflows/production-beaker-cd.yaml similarity index 68% rename from examples/generators/production_beaker_smart_contract_typescript/.github/workflows/cd.yaml rename to examples/production_beaker/.github/workflows/production-beaker-cd.yaml index f4a5dc7f..a5b2f8cd 100644 --- a/examples/generators/production_beaker_smart_contract_typescript/.github/workflows/cd.yaml +++ b/examples/production_beaker/.github/workflows/production-beaker-cd.yaml @@ -1,21 +1,19 @@ -name: Continuous Delivery of Smart Contract +name: Release production_beaker on: + workflow_call: push: branches: - main -concurrency: release - jobs: - ci-check: - name: Perform Checks - uses: ./.github/workflows/checks.yaml - + validate: + name: Validate production_beaker + uses: ./.github/workflows/production-beaker-ci.yaml deploy-testnet: - runs-on: 'ubuntu-latest' - needs: ci-check - environment: Test + runs-on: "ubuntu-latest" + needs: validate + environment: contract-testnet steps: - name: Checkout source code uses: actions/checkout@v4 @@ -26,14 +24,14 @@ jobs: - name: Set up Python 3.12 uses: actions/setup-python@v5 with: - python-version: '3.12' - cache: 'poetry' + python-version: "3.12" + cache: "poetry" - name: Install algokit run: pipx install algokit - name: Bootstrap dependencies - run: algokit bootstrap all + run: algokit bootstrap all --project-name 'production_beaker' - name: Configure git shell: bash @@ -42,7 +40,7 @@ jobs: git config --global user.email "actions@github.com" && git config --global user.name "github-actions" - name: Deploy to testnet - run: algokit deploy testnet + run: algokit deploy testnet --project-name 'production_beaker' env: # This is the account that becomes the creator of the contract DEPLOYER_MNEMONIC: ${{ secrets.DEPLOYER_MNEMONIC }} diff --git a/examples/production_beaker/.github/workflows/production-beaker-ci.yaml b/examples/production_beaker/.github/workflows/production-beaker-ci.yaml new file mode 100644 index 00000000..f5011eb3 --- /dev/null +++ b/examples/production_beaker/.github/workflows/production-beaker-ci.yaml @@ -0,0 +1,60 @@ +name: Validate production_beaker + +on: + workflow_call: + pull_request: + +jobs: + validate: + runs-on: "ubuntu-latest" + steps: + - name: Checkout source code + uses: actions/checkout@v4 + + - name: Install poetry + run: pipx install poetry + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: "poetry" + + - name: Install algokit + run: pipx install algokit + + - name: Start LocalNet + run: algokit localnet start + + - name: Bootstrap dependencies + run: algokit bootstrap all --project-name 'production_beaker' + + - name: Configure git + shell: bash + run: | + # set git user and email as test invoke git + git config --global user.email "actions@github.com" && git config --global user.name "github-actions" + + - name: Audit python dependencies + run: algokit project run audit --project-name 'production_beaker' + + - name: Lint and format python dependencies + run: algokit project run lint --project-name 'production_beaker' + + - name: Run tests + shell: bash + run: | + set -o pipefail + algokit project run test --project-name 'production_beaker' + + - name: Build smart contracts + run: algokit project run build --project-name 'production_beaker' + + - name: Scan TEAL files for issues + run: algokit project run audit-teal --project-name 'production_beaker' + + - name: Check output stability of the smart contracts + run: algokit project run ci-teal-diff --project-name 'production_beaker' + + - name: Run deployer against LocalNet + run: algokit project deploy localnet --project-name 'production_beaker' diff --git a/examples/production_beaker/README.md b/examples/production_beaker/README.md index 79af52d8..a647f6da 100644 --- a/examples/production_beaker/README.md +++ b/examples/production_beaker/README.md @@ -43,7 +43,9 @@ For an interactive guided walkthrough of the project install [CodeTour](https:// > For guidance on `smart_contracts` folder and adding new contracts to the project please see [README](smart_contracts/README.md) on the respective folder.### Continuous Integration / Continuous Deployment (CI/CD) -This project uses [GitHub Actions](https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions) to define CI/CD workflows, which are located in the [`.github/workflows`](./.github/workflows) folder. +This project uses [GitHub Actions](https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions) to define CI/CD workflows, which are located in the [.github/workflows](`.github/workflows`) folder. + +> Please note, if you instantiated the project with --workspace flag in `algokit init` it will automatically attempt to move the contents of the `.github` folder to the root of the workspace. ### Debugging Smart Contracts diff --git a/examples/production_beaker/smart_contracts/config.py b/examples/production_beaker/smart_contracts/config.py index 3a1d85fe..89d948da 100644 --- a/examples/production_beaker/smart_contracts/config.py +++ b/examples/production_beaker/smart_contracts/config.py @@ -12,9 +12,10 @@ @dataclasses.dataclass class SmartContract: app: Application - deploy: Callable[ - [AlgodClient, IndexerClient, ApplicationSpecification, Account], None - ] | None = None + deploy: ( + Callable[[AlgodClient, IndexerClient, ApplicationSpecification, Account], None] + | None + ) = None def import_contract(folder: Path) -> Application: diff --git a/examples/production_beaker/smart_contracts/hello_world/deploy_config.py b/examples/production_beaker/smart_contracts/hello_world/deploy_config.py index 7ed7ad6c..46eeb59e 100644 --- a/examples/production_beaker/smart_contracts/hello_world/deploy_config.py +++ b/examples/production_beaker/smart_contracts/hello_world/deploy_config.py @@ -30,9 +30,11 @@ def deploy( if is_mainnet else algokit_utils.OnSchemaBreak.ReplaceApp ), - on_update=algokit_utils.OnUpdate.AppendApp - if is_mainnet - else algokit_utils.OnUpdate.UpdateApp, + on_update=( + algokit_utils.OnUpdate.AppendApp + if is_mainnet + else algokit_utils.OnUpdate.UpdateApp + ), allow_delete=not is_mainnet, allow_update=not is_mainnet, ) diff --git a/examples/starter_beaker/.algokit.toml b/examples/starter_beaker/.algokit.toml index f6f7f660..e1cd14c4 100644 --- a/examples/starter_beaker/.algokit.toml +++ b/examples/starter_beaker/.algokit.toml @@ -1,19 +1,38 @@ [algokit] -min_version = "v1.10.0" +min_version = "v2.0.0" -[deploy] +[generate.smart_contract] +description = "Adds new smart contract to existing project" +path = ".algokit/generators/create_contract" + +[project] +type = 'contract' +name = 'starter_beaker' +artifacts = 'smart_contracts/artifacts' + +[project.deploy] command = "poetry run python -m smart_contracts deploy" environment_secrets = [ "DEPLOYER_MNEMONIC", ] -[deploy.localnet] +[project.deploy.localnet] environment_secrets = [] -[generate.smart_contract] -description = "Adds new smart contract to existing project" -path = ".algokit/generators/create_contract" +[project.run] +# Commands intented for use locally and in CI +build = { commands = [ + 'poetry run python -m smart_contracts build', +], description = 'Build all smart contracts in the project' } +lint = { commands = [ +], description = 'Perform linting' } +audit-teal = { commands = [ + # 🚨 IMPORTANT 🚨: For strict TEAL validation, remove --exclude statements. The default starter contract is not for production. Ensure thorough testing and adherence to best practices in smart contract development. This is not a replacement for a professional audit. + 'algokit task analyze smart_contracts/artifacts --recursive --force --exclude rekey-to --exclude is-updatable --exclude missing-fee-check --exclude is-deletable --exclude can-close-asset --exclude can-close-account --exclude unprotected-deletable --exclude unprotected-updatable', +], description = 'Audit TEAL files' } -[project] -type = 'contract' -name = 'starter_beaker' +# Commands indented for CI only, prefixed with `ci-` by convention +ci-teal-diff = { commands = [ + 'git add -N ./smart_contracts/artifacts', + 'git diff --exit-code --minimal ./smart_contracts/artifacts', +], description = 'Check TEAL files for differences' } diff --git a/examples/starter_beaker/smart_contracts/config.py b/examples/starter_beaker/smart_contracts/config.py index 3a1d85fe..89d948da 100644 --- a/examples/starter_beaker/smart_contracts/config.py +++ b/examples/starter_beaker/smart_contracts/config.py @@ -12,9 +12,10 @@ @dataclasses.dataclass class SmartContract: app: Application - deploy: Callable[ - [AlgodClient, IndexerClient, ApplicationSpecification, Account], None - ] | None = None + deploy: ( + Callable[[AlgodClient, IndexerClient, ApplicationSpecification, Account], None] + | None + ) = None def import_contract(folder: Path) -> Application: diff --git a/includes/project_name_kebab.jinja b/includes/project_name_kebab.jinja new file mode 100644 index 00000000..9bd9740b --- /dev/null +++ b/includes/project_name_kebab.jinja @@ -0,0 +1 @@ +{{- project_name | trim | replace("_", "-") | replace(" ", "-") | lower() | regex_replace('[-]+', '-') | regex_replace('[^a-z0-9.-]', '') -}} diff --git a/template_content/.algokit.toml.jinja b/template_content/.algokit.toml.jinja index 8990800d..909eca01 100644 --- a/template_content/.algokit.toml.jinja +++ b/template_content/.algokit.toml.jinja @@ -1,7 +1,16 @@ [algokit] -min_version = "v1.10.0" +min_version = "v2.0.0" -[deploy] +[generate.smart_contract] +description = "Adds new smart contract to existing project" +path = ".algokit/generators/create_contract" + +[project] +type = 'contract' +name = '{{ project_name }}' +artifacts = 'smart_contracts/artifacts' + +[project.deploy] {%- if deployment_language == 'python' %} command = "poetry run python -m smart_contracts deploy" {%- elif deployment_language == 'typescript' %} @@ -14,13 +23,49 @@ environment_secrets = [ {%- endif %} ] -[deploy.localnet] +[project.deploy.localnet] environment_secrets = [] -[generate.smart_contract] -description = "Adds new smart contract to existing project" -path = ".algokit/generators/create_contract" +[project.run] +# Commands intented for use locally and in CI +build = { commands = [ + 'poetry run python -m smart_contracts build', +], description = 'Build all smart contracts in the project' } +{%- if deployment_language == 'python' and use_python_pytest %} +test = { commands = [ + 'poetry run pytest', +], description = 'Run smart contract tests' } +{%- elif deployment_language == 'typescript' and use_typescript_jest %} +test = { commands = [ + 'npm run test', +], description = 'Run smart contract tests using Jest' } +{%- endif %} +{%- if use_python_pip_audit %} +audit = { commands = [ + 'poetry export --without=dev -o requirements.txt', + 'poetry run pip-audit -r requirements.txt', +], description = 'Audit with pip-audit' } +{%- endif %} +lint = { commands = [ +{%- if use_python_black %} + 'poetry run black --check .', +{%- endif %} +{%- if python_linter == 'ruff' %} + 'poetry run ruff .', +{%- elif python_linter == 'flake8' %} + 'poetry run flake8 .', +{%- endif %} +{%- if use_python_mypy %} + 'poetry run mypy', +{%- endif %} +], description = 'Perform linting' } +audit-teal = { commands = [ + # 🚨 IMPORTANT 🚨: For strict TEAL validation, remove --exclude statements. The default starter contract is not for production. Ensure thorough testing and adherence to best practices in smart contract development. This is not a replacement for a professional audit. + 'algokit task analyze smart_contracts/artifacts --recursive --force --exclude rekey-to --exclude is-updatable --exclude missing-fee-check --exclude is-deletable --exclude can-close-asset --exclude can-close-account --exclude unprotected-deletable --exclude unprotected-updatable', +], description = 'Audit TEAL files' } -[project] -type = 'contract' -name = '{{ project_name }}' +# Commands indented for CI only, prefixed with `ci-` by convention +ci-teal-diff = { commands = [ + 'git add -N ./smart_contracts/artifacts', + 'git diff --exit-code --minimal ./smart_contracts/artifacts', +], description = 'Check TEAL files for differences' } diff --git a/template_content/.algokit/generators/create_contract/smart_contracts/{% raw %}{{ contract_name }}{% endraw %}/{% if deployment_language == 'python' %}deploy_config.py.j2{% endif %}.jinja b/template_content/.algokit/generators/create_contract/smart_contracts/{% raw %}{{ contract_name }}{% endraw %}/{% if deployment_language == 'python' %}deploy_config.py.j2{% endif %}.jinja index c86ec1f8..ebfcf6d3 100644 --- a/template_content/.algokit/generators/create_contract/smart_contracts/{% raw %}{{ contract_name }}{% endraw %}/{% if deployment_language == 'python' %}deploy_config.py.j2{% endif %}.jinja +++ b/template_content/.algokit/generators/create_contract/smart_contracts/{% raw %}{{ contract_name }}{% endraw %}/{% if deployment_language == 'python' %}deploy_config.py.j2{% endif %}.jinja @@ -39,9 +39,11 @@ def deploy( if is_mainnet else algokit_utils.OnSchemaBreak.ReplaceApp ), - on_update=algokit_utils.OnUpdate.AppendApp - if is_mainnet - else algokit_utils.OnUpdate.UpdateApp, + on_update=( + algokit_utils.OnUpdate.AppendApp + if is_mainnet + else algokit_utils.OnUpdate.UpdateApp + ), allow_delete=not is_mainnet, allow_update=not is_mainnet, ) diff --git a/template_content/README.md.jinja b/template_content/README.md.jinja index 465e8773..d8eea8f7 100644 --- a/template_content/README.md.jinja +++ b/template_content/README.md.jinja @@ -49,7 +49,9 @@ For an interactive guided walkthrough of the project install [CodeTour](https:// {%- if use_github_actions -%} ### Continuous Integration / Continuous Deployment (CI/CD) -This project uses [GitHub Actions](https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions) to define CI/CD workflows, which are located in the [`.github/workflows`](./.github/workflows) folder. +This project uses [GitHub Actions](https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions) to define CI/CD workflows, which are located in the [.github/workflows](`{% if use_workspace %}../../.github/workflows{% else %}.github/workflows{% endif %}`) folder. + +> Please note, if you instantiated the project with --workspace flag in `algokit init` it will automatically attempt to move the contents of the `.github` folder to the root of the workspace. ### Debugging Smart Contracts diff --git a/template_content/smart_contracts/config.py.jinja b/template_content/smart_contracts/config.py.jinja index 3a1d85fe..89d948da 100644 --- a/template_content/smart_contracts/config.py.jinja +++ b/template_content/smart_contracts/config.py.jinja @@ -12,9 +12,10 @@ from beaker import Application @dataclasses.dataclass class SmartContract: app: Application - deploy: Callable[ - [AlgodClient, IndexerClient, ApplicationSpecification, Account], None - ] | None = None + deploy: ( + Callable[[AlgodClient, IndexerClient, ApplicationSpecification, Account], None] + | None + ) = None def import_contract(folder: Path) -> Application: diff --git a/template_content/smart_contracts/{{ contract_name }}/{% if deployment_language == 'python' %}deploy_config.py{% endif %}.jinja b/template_content/smart_contracts/{{ contract_name }}/{% if deployment_language == 'python' %}deploy_config.py{% endif %}.jinja index 239e90b0..c00a72e6 100644 --- a/template_content/smart_contracts/{{ contract_name }}/{% if deployment_language == 'python' %}deploy_config.py{% endif %}.jinja +++ b/template_content/smart_contracts/{{ contract_name }}/{% if deployment_language == 'python' %}deploy_config.py{% endif %}.jinja @@ -38,9 +38,11 @@ def deploy( if is_mainnet else algokit_utils.OnSchemaBreak.ReplaceApp ), - on_update=algokit_utils.OnUpdate.AppendApp - if is_mainnet - else algokit_utils.OnUpdate.UpdateApp, + on_update=( + algokit_utils.OnUpdate.AppendApp + if is_mainnet + else algokit_utils.OnUpdate.UpdateApp + ), allow_delete=not is_mainnet, allow_update=not is_mainnet, ) diff --git a/template_content/{% if use_github_actions %}.github{% endif %}/workflows/checks.yaml.jinja b/template_content/{% if use_github_actions %}.github{% endif %}/workflows/checks.yaml.jinja deleted file mode 100644 index 0d268fac..00000000 --- a/template_content/{% if use_github_actions %}.github{% endif %}/workflows/checks.yaml.jinja +++ /dev/null @@ -1,108 +0,0 @@ -name: Check code base - -on: - workflow_call: - -jobs: - checks: - runs-on: 'ubuntu-latest' - steps: - - name: Checkout source code - uses: actions/checkout@v4 - - - name: Install poetry - run: pipx install poetry - - - name: Set up Python 3.12 - uses: actions/setup-python@v5 - with: - python-version: '3.12' - cache: 'poetry' - - - name: Install algokit - run: pipx install algokit - - - name: Start LocalNet - run: algokit localnet start - - - name: Bootstrap dependencies - run: algokit bootstrap all - - - name: Configure git - shell: bash - run: | - # set git user and email as test invoke git - git config --global user.email "actions@github.com" && git config --global user.name "github-actions" -{%- if use_python_pip_audit %} - - - name: Audit with pip-audit - run: | - # audit non dev dependencies, no exclusions - poetry export --without=dev > requirements.txt && poetry run pip-audit -r requirements.txt - - # audit all dependencies, with exclusions. - # If a vulnerability is found in a dev dependency without an available fix, - # it can be temporarily ignored by adding --ignore-vuln e.g. - # --ignore-vuln "GHSA-hcpj-qp55-gfph" # GitPython vulnerability, dev only dependency - poetry run pip-audit -{%- endif %} -{%- if use_python_black %} - - - name: Check formatting with Black - run: | - # stop the build if there are files that don't meet formatting requirements - poetry run black --check . -{%- endif %} -{%- if python_linter == 'ruff' %} - - - name: Check linting with Ruff - run: | - # stop the build if there are Python syntax errors or undefined names - poetry run ruff . -{%- elif python_linter == 'flake8' %} - - - name: Check linting with flake8 - run: | - # stop the build if there are Python syntax errors or undefined names - poetry run flake8 . -{%- endif %} - - - name: Build smart contracts - run: poetry run python -m smart_contracts build -{%- if use_python_mypy %} - - - name: Check types with mypy - run: poetry run mypy -{%- endif %} -{%- if use_python_pytest %} - - - name: Run tests - shell: bash - run: | - set -o pipefail - poetry run pytest --junitxml=pytest-junit.xml -{%- endif %} -{%- if use_typescript_jest %} - - - name: Run tests - shell: bash - run: npm run test -{%- endif %} - - - name: Scan TEAL files for issues - run: algokit task analyze .algokit --recursive --force # add --diff flag if you want output stability checks instead - - - name: Check output stability of the smart contracts - shell: bash - run: | - # Add untracked files as empty so they come up in diff - git add -N ./smart_contracts/artifacts - # Error out if there are any changes in teal after generating output - git diff --exit-code --minimal ./smart_contracts/artifacts || (echo "::error ::Smart contract artifacts have changed, ensure committed artifacts are up to date" && exit 1); - - - name: Run deployer against LocalNet -{%- if deployment_language == 'typescript' %} - run: npm run deploy:ci -{%- elif deployment_language == 'python' %} - run: poetry run python -m smart_contracts deploy -{%- endif %} diff --git a/template_content/{% if use_github_actions %}.github{% endif %}/workflows/pr.yaml b/template_content/{% if use_github_actions %}.github{% endif %}/workflows/pr.yaml deleted file mode 100644 index a80f7840..00000000 --- a/template_content/{% if use_github_actions %}.github{% endif %}/workflows/pr.yaml +++ /dev/null @@ -1,8 +0,0 @@ -name: Pull Request validation - -on: [pull_request] - -jobs: - pr-check: - name: Perform Checks - uses: ./.github/workflows/checks.yaml diff --git a/template_content/{% if use_github_actions %}.github{% endif %}/workflows/cd.yaml.jinja b/template_content/{% if use_github_actions %}.github{% endif %}/workflows/{% include pathjoin('includes', 'project_name_kebab.jinja') %}-cd.yaml.jinja similarity index 69% rename from template_content/{% if use_github_actions %}.github{% endif %}/workflows/cd.yaml.jinja rename to template_content/{% if use_github_actions %}.github{% endif %}/workflows/{% include pathjoin('includes', 'project_name_kebab.jinja') %}-cd.yaml.jinja index 5a8e0fe2..eb5af8ac 100644 --- a/template_content/{% if use_github_actions %}.github{% endif %}/workflows/cd.yaml.jinja +++ b/template_content/{% if use_github_actions %}.github{% endif %}/workflows/{% include pathjoin('includes', 'project_name_kebab.jinja') %}-cd.yaml.jinja @@ -1,21 +1,24 @@ -name: Continuous Delivery of Smart Contract +name: Release {{ project_name }} on: + workflow_call: + + {%- if not use_workspace %} push: branches: - main - -concurrency: release + {%- endif %} jobs: - ci-check: - name: Perform Checks - uses: ./.github/workflows/checks.yaml - + {%- if not use_workspace %} + validate: + name: Validate {{ project_name }} + uses: ./.github/workflows/{% include pathjoin('includes', 'project_name_kebab.jinja') %}-ci.yaml + {%- endif %} deploy-testnet: - runs-on: 'ubuntu-latest' - needs: ci-check - environment: Test + runs-on: "ubuntu-latest" + {% if not use_workspace %}needs: validate{% endif %} + environment: contract-testnet steps: - name: Checkout source code uses: actions/checkout@v4 @@ -26,14 +29,14 @@ jobs: - name: Set up Python 3.12 uses: actions/setup-python@v5 with: - python-version: '3.12' - cache: 'poetry' + python-version: "3.12" + cache: "poetry" - name: Install algokit run: pipx install algokit - name: Bootstrap dependencies - run: algokit bootstrap all + run: algokit bootstrap all --project-name '{{ project_name }}' - name: Configure git shell: bash @@ -42,7 +45,7 @@ jobs: git config --global user.email "actions@github.com" && git config --global user.name "github-actions" - name: Deploy to testnet - run: algokit deploy testnet + run: algokit deploy testnet --project-name '{{ project_name }}' env: {%- if use_dispenser %} # This is the account that becomes the creator of the contract diff --git a/template_content/{% if use_github_actions %}.github{% endif %}/workflows/{% include pathjoin('includes', 'project_name_kebab.jinja') %}-ci.yaml.jinja b/template_content/{% if use_github_actions %}.github{% endif %}/workflows/{% include pathjoin('includes', 'project_name_kebab.jinja') %}-ci.yaml.jinja new file mode 100644 index 00000000..03dae947 --- /dev/null +++ b/template_content/{% if use_github_actions %}.github{% endif %}/workflows/{% include pathjoin('includes', 'project_name_kebab.jinja') %}-ci.yaml.jinja @@ -0,0 +1,63 @@ +name: Validate {{ project_name }} + +on: + workflow_call: + + {%- if not use_workspace %} + pull_request: + {%- endif %} + +jobs: + validate: + runs-on: "ubuntu-latest" + steps: + - name: Checkout source code + uses: actions/checkout@v4 + + - name: Install poetry + run: pipx install poetry + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: "poetry" + + - name: Install algokit + run: pipx install algokit + + - name: Start LocalNet + run: algokit localnet start + + - name: Bootstrap dependencies + run: algokit bootstrap all --project-name '{{ project_name }}' + + - name: Configure git + shell: bash + run: | + # set git user and email as test invoke git + git config --global user.email "actions@github.com" && git config --global user.name "github-actions" + + - name: Audit python dependencies + run: algokit project run audit --project-name '{{ project_name }}' + + - name: Lint and format python dependencies + run: algokit project run lint --project-name '{{ project_name }}' + + - name: Run tests + shell: bash + run: | + set -o pipefail + algokit project run test --project-name '{{ project_name }}' + + - name: Build smart contracts + run: algokit project run build --project-name '{{ project_name }}' + + - name: Scan TEAL files for issues + run: algokit project run audit-teal --project-name '{{ project_name }}' + + - name: Check output stability of the smart contracts + run: algokit project run ci-teal-diff --project-name '{{ project_name }}' + + - name: Run deployer against LocalNet + run: algokit project deploy localnet --project-name '{{ project_name }}' diff --git a/tests/test_generators.py b/tests/test_generators.py index 61972152..9103a82e 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -20,12 +20,9 @@ } config_path = Path(__file__).parent.parent / "pyproject.toml" BLACK_ARGS = ["black", "--check", "--diff", "--config", str(config_path), "."] -RUFF_ARGS = ["ruff", "--diff", "--config", str(config_path), "."] -MYPY_ARGS = [ - "mypy", - "--ignore-missing-imports", # TODO: only ignore missing typed clients in config.py - ".", -] +BUILD_ARGS = ["algokit", "project", "run", "build"] +TEST_ARGS = ["algokit", "project", "run", "test"] +LINT_ARGS = ["algokit", "project", "run", "lint"] def _load_copier_yaml(path: Path) -> dict[str, str | bool | dict]: @@ -52,7 +49,12 @@ def working_dir() -> Iterator[Path]: dest_dir = generated_root / src_dir.stem shutil.rmtree(dest_dir, ignore_errors=True) - shutil.copytree(src_dir, dest_dir, dirs_exist_ok=True) + shutil.copytree( + src_dir, + dest_dir, + dirs_exist_ok=True, + ignore=shutil.ignore_patterns(".*_cache", ".venv", "__pycache__"), + ) def run_init( @@ -88,7 +90,6 @@ def run_init( "--defaults", "--no-ide", "--no-git", - "--no-bootstrap", "--no-workspace", ] answers = {**DEFAULT_PARAMETERS, **(answers or {})} @@ -121,13 +122,13 @@ def check_codebase(working_dir: Path, test_name: str) -> subprocess.CompletedPro content = src_path_pattern.sub("_src_path: ", content) copier_answers.write_text(content, "utf-8") - check_args = [BLACK_ARGS] + check_args = [BLACK_ARGS, BUILD_ARGS] # Starter template does not have ruff config or mypy config by default # so only check for them if the starter template is not used processed_questions = _load_copier_yaml(copier_answers) if processed_questions["preset_name"] == "production": - check_args += [RUFF_ARGS, MYPY_ARGS] + check_args += [LINT_ARGS, TEST_ARGS] for check_arg in check_args: result = subprocess.run( diff --git a/tests/test_templates.py b/tests/test_templates.py index 02a8fbde..aa5e30a4 100644 --- a/tests/test_templates.py +++ b/tests/test_templates.py @@ -19,13 +19,10 @@ "author_email": "None", } config_path = Path(__file__).parent.parent / "pyproject.toml" -BLACK_ARGS = ["black", "--check", "--diff", "--config", str(config_path), "."] -RUFF_ARGS = ["ruff", "--diff", "--config", str(config_path), "."] -MYPY_ARGS = [ - "mypy", - "--ignore-missing-imports", # TODO: only ignore missing typed clients in config.py - ".", -] +BASELINE_LINT_ARGS = ["black", "--check", "--diff", "--config", str(config_path), "."] +BUILD_ARGS = ["algokit", "project", "run", "build"] +TEST_ARGS = ["algokit", "project", "run", "test"] +LINT_ARGS = ["algokit", "project", "run", "lint"] def _load_copier_yaml(path: Path) -> dict[str, str | bool | dict]: @@ -52,7 +49,12 @@ def working_dir() -> Iterator[Path]: dest_dir = generated_root / src_dir.stem shutil.rmtree(dest_dir, ignore_errors=True) - shutil.copytree(src_dir, dest_dir, dirs_exist_ok=True) + shutil.copytree( + src_dir, + dest_dir, + dirs_exist_ok=True, + ignore=shutil.ignore_patterns(".*_cache", ".venv", "__pycache__"), + ) def run_init( @@ -88,7 +90,6 @@ def run_init( "--defaults", "--no-ide", "--no-git", - "--no-bootstrap", "--no-workspace", ] answers = {**DEFAULT_PARAMETERS, **(answers or {})} @@ -118,13 +119,13 @@ def run_init( content = src_path_pattern.sub("_src_path: ", content) copier_answers.write_text(content, "utf-8") - check_args = [BLACK_ARGS] + check_args = [BUILD_ARGS, BASELINE_LINT_ARGS] # Starter template does not have ruff config or mypy config by default # so only check for them if the starter template is not used processed_questions = _load_copier_yaml(copier_answers) if processed_questions["preset_name"] == "production": - check_args += [RUFF_ARGS, MYPY_ARGS] + check_args += [LINT_ARGS, TEST_ARGS] for check_arg in check_args: result = subprocess.run(