diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 4d87454..b890b3d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -6,17 +6,13 @@ "ghcr.io/devcontainers/features/common-utils:2": { "configureZshAsDefaultShell": false }, - "ghcr.io/devcontainers/features/azure-cli:1": { - "version": "2.82.0", - "installBicep": true + "ghcr.io/eitsupi/devcontainer-features/jq-likes:2": { + // ? move to Taskfile? + "jqVersion": "latest", + "yqVersion": "latest", + "gojqVersion": "latest", + "jaqVersion": "latest" }, - "ghcr.io/devcontainers/features/terraform:1": { - "version": "1.10.4" - }, - "ghcr.io/devcontainers/features/go:1": { - "version": "1.25.5" - }, - "ghcr.io/devcontainers/features/powershell:1": {}, "ghcr.io/eitsupi/devcontainer-features/go-task:1": {} }, "workspaceMount": "source=${localWorkspaceFolder},target=${containerWorkspaceFolder},type=bind,consistency=cached", @@ -24,14 +20,48 @@ "remoteEnv": { "LOCAL_WORKSPACE_FOLDER": "${localWorkspaceFolder}" }, + "onCreateCommand": { + "init": "task init" + }, "postCreateCommand": { "git-safe-dir": "git config --global --add safe.directory ${containerWorkspaceFolder}" }, "customizations": { "vscode": { + "settings": { + "terminal.integrated.defaultProfile.linux": "bash", + "terminal.integrated.defaultProfile.osx": "zsh", + "terminal.integrated.defaultProfile.windows": "PowerShell", + "powershell.powerShellAdditionalExePaths": { + "pwsh": "/usr/bin/pwsh" + }, + "powershell.powerShellDefaultVersion": "pwsh", + "shellformat.path": "/home/${remoteUser}/.local/bin/shfmt" + }, "extensions": [ + "redhat.vscode-yaml", + "golang.go", + "ms-vscode.powershell", "ms-azuretools.vscode-bicep", - "hashicorp.terraform" + "edwinhuish.better-comments-next", + "timonwong.shellcheck", + "task.vscode-task", + "esbenp.prettier-vscode", + "fnando.linter", + "ms-azuretools.vscode-azure-github-copilot", + "ms-azuretools.vscode-docker", + "davidanson.vscode-markdownlint", + "hashicorp.terraform", + "github.copilot", + "github.copilot-chat", + "github.vscode-github-actions", + "github.vscode-pull-request-github", + "github.codespaces", + "github.remotehub", + "bierner.github-markdown-preview", + "usernamehw.errorlens", + "lumirelle.shell-format-rev", + "editorconfig.editorconfig" ] } } diff --git a/.env.sh b/.env.sh deleted file mode 100644 index 8635594..0000000 --- a/.env.sh +++ /dev/null @@ -1,10 +0,0 @@ -SUBSCRIPTION_ID="YOUR_SUBSCRIPTION_ID" -TENANT_ID="YOUR_TENANT_ID" -# TEST_DEPLOYMENT_NAME_PFX="testDeploy" -# TEST_DEPLOYMENT_RESOURCE_GROUP_NAME_PFX="testdeployrg" -SP_CLIENT_ID="YOUR_SP_CLIENT_ID" -SP_CLIENT_SECRET="YOUR_SP_CLIENT_SECRET" -SP_OBJECT_ID="YOUR_SP_OBJECT_ID" -TEMPLATE_FILE="./templates/samples/aks.json" -PARAMETERS_FILE="./templates/samples/aks-parameters.json" - diff --git a/.github/linters/.lychee.toml b/.github/linters/.lychee.toml deleted file mode 100644 index e27d79c..0000000 --- a/.github/linters/.lychee.toml +++ /dev/null @@ -1,60 +0,0 @@ -# https://lychee.cli.rs/#/usage/config -# Example config: https://github.com/lycheeverse/lychee/blob/master/lychee.example.toml - - -############################# Cache ############################### - -# Enable link caching. This can be helpful to avoid checking the same links on multiple runs. -cache = true - -# Discard all cached requests older than this duration. -max_cache_age = "1d" - -############################# Runtime ############################# - -# Maximum number of allowed redirects. -max_redirects = 6 - -# Maximum number of allowed retries before a link is declared dead. -max_retries = 2 - -# Maximum number of concurrent link checks. -# max_concurrency = 2 - -############################# Requests ############################ - -# User agent to send with each request. -user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0" - -# Website timeout from connect to response finished. -timeout = 45 - -# Minimum wait time in seconds between retries of failed requests. -retry_wait_time = 2 - -# Comma-separated list of accepted status codes for valid links. -accept = ["200", "206", "301", "429"] - -# Only test links with the given schemes (e.g. https). -# Omit to check links with any scheme. -scheme = ["https", "http", "file"] - -# Custom request headers -headers = ['Accept-Encoding: deflate, compress, gzip, br, zstd'] - -############################# Exclusions ########################## - -# Ignore case of paths when matching glob patterns. -glob_ignore_case = true - -# Exclude all private IPs from checking. -exclude_all_private = true - -# Exclude private IP address ranges from checking. -exclude_private = true - -# Exclude link-local IP address range from checking. -exclude_link_local = true - -# Exclude loopback IP address range and localhost from checking. -exclude_loopback = true diff --git a/.github/linters/.markdownlint-cli2.yaml b/.github/linters/.markdownlint-cli2.yaml index de7ecff..b5eff52 100644 --- a/.github/linters/.markdownlint-cli2.yaml +++ b/.github/linters/.markdownlint-cli2.yaml @@ -7,3 +7,5 @@ ignores: - .git - "**/node_modules/**" - .copilot-tracking/** + - venv/** + - .venv/** diff --git a/.github/linters/.markdownlint.yml b/.github/linters/.markdownlint.yml index af78481..91fe5ab 100644 --- a/.github/linters/.markdownlint.yml +++ b/.github/linters/.markdownlint.yml @@ -30,7 +30,7 @@ MD029: # MD033/no-inline-html - Inline HTML MD033: # Allowed elements - allowed_elements: [br, pre, a] + allowed_elements: [br, pre] # MD036/no-emphasis-as-heading - Emphasis used instead of a heading MD036: false diff --git a/.github/linters/.prettierrc.yml b/.github/linters/.prettierrc.yml new file mode 100644 index 0000000..fe8877d --- /dev/null +++ b/.github/linters/.prettierrc.yml @@ -0,0 +1,38 @@ +# yaml-language-server: $schema=https://www.schemastore.org/prettierrc.json +--- +arrowParens: always +bracketSpacing: true +endOfLine: lf +htmlWhitespaceSensitivity: css +insertPragma: false # consider true +singleAttributePerLine: false +bracketSameLine: false +jsxSingleQuote: true +printWidth: 120 +proseWrap: preserve +quoteProps: as-needed +requirePragma: false # consider true +semi: true +singleQuote: true +tabWidth: 2 +trailingComma: none +useTabs: false +vueIndentScriptAndStyle: true +embeddedLanguageFormatting: auto +experimentalTernaries: true +experimentalOperatorPosition: end +# multilineArraysWrapThreshold: 1 +# plugins: +# - prettier-plugin-multiline-arrays + +# Language-specific overrides +overrides: + - files: "*.md" + options: + proseWrap: always + - files: "*.yml" + options: + singleQuote: false + - files: "*.yaml" + options: + singleQuote: false diff --git a/.github/linters/.shellcheckrc b/.github/linters/.shellcheckrc new file mode 100644 index 0000000..fe8e96e --- /dev/null +++ b/.github/linters/.shellcheckrc @@ -0,0 +1,3 @@ +# .shellcheckrc + +disable=SC3037,SC2086,SC2155 diff --git a/.github/linters/.yamllint.yml b/.github/linters/.yamllint.yml new file mode 100644 index 0000000..2202ca8 --- /dev/null +++ b/.github/linters/.yamllint.yml @@ -0,0 +1,29 @@ +# yaml-language-server: $schema=https://www.schemastore.org/yamllint.json +# docs: https://yamllint.readthedocs.io/en/stable/configuration.html#extending-the-default-configuration +--- +extends: default + +locale: en_US.UTF-8 + +rules: + document-start: + level: warning + ignore: + - .cspell.yml + line-length: disable + quoted-strings: + level: error + quote-type: double + required: only-when-needed + comments: + min-spaces-from-content: 1 + braces: + level: warning + min-spaces-inside: 0 + max-spaces-inside: 1 + truthy: + check-keys: false + +ignore-from-file: + - .gitignore + - .yamlignore diff --git a/.github/linters/actionlint.yaml b/.github/linters/actionlint.yaml new file mode 100644 index 0000000..09a68ad --- /dev/null +++ b/.github/linters/actionlint.yaml @@ -0,0 +1,20 @@ +--- +self-hosted-runner: + # Labels of self-hosted runner in array of strings. + labels: [] + +# Configuration variables in array of strings defined in your repository or +# organization. `null` means disabling configuration variables check. +# Empty array means no configuration variable is allowed. +config-variables: null + +# Configuration for file paths. The keys are glob patterns to match to file +# paths relative to the repository root. The values are the configurations for +# the file paths. Note that the path separator is always '/'. +# The following configurations are available. +# +# "ignore" is an array of regular expression patterns. Matched error messages +# are ignored. This is similar to the "-ignore" command line option. +paths: +# .github/workflows/**/*.yml: +# ignore: [] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a5f9417..ea072f6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,12 +14,16 @@ on: - main types: - opened + - reopened - synchronize + - ready_for_review merge_group: # schedule: # - cron: "0 2 * * *" workflow_dispatch: +permissions: {} + concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true @@ -27,76 +31,7 @@ concurrency: env: CLI_VERSION: ${{ github.sha }} -permissions: {} - jobs: - lint-go: - name: ๐Ÿงน Lint Go - runs-on: ubuntu-24.04 - timeout-minutes: 10 - permissions: - contents: read - pull-requests: read - steps: - - name: โคต๏ธ Checkout - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - with: - persist-credentials: false - - - name: ๐Ÿšง Setup Go - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 - with: - go-version-file: go.mod - cache: true - - - name: ๐Ÿšง Setup Task - uses: go-task/setup-task@0ab1b2a65bc55236a3bc64cde78f80e20e8885c2 # v1.0.0 - - - name: ๐Ÿ”€ Get dependencies - run: task deps - - - name: โœ”๏ธ Run GoVulnCheck - run: | - task install:govulncheck - task govulncheck || (echo "::warning::govulncheck found issues" && exit 0) - - - name: โœ”๏ธ Run Go linters - uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0 - with: - version: latest - only-new-issues: true - skip-cache: true - skip-save-cache: true - problem-matchers: true - - lint-markdown: - name: ๐Ÿงน Lint Markdown - runs-on: ubuntu-24.04 - timeout-minutes: 10 - permissions: - contents: read - steps: - - name: โคต๏ธ Checkout - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - with: - persist-credentials: false - - - name: ๐Ÿšง Setup Go - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 - with: - go-version-file: go.mod - cache: true - - - name: ๐Ÿšง Setup Task - uses: go-task/setup-task@0ab1b2a65bc55236a3bc64cde78f80e20e8885c2 # v1.0.0 - - - name: ๐Ÿ”จ Setup tools - run: | - task install:markdownlint - - - name: โœ”๏ธ Run Files linters - run: task lint:md - build: name: ๐Ÿ—๏ธ Build runs-on: ubuntu-24.04 @@ -122,7 +57,7 @@ jobs: run: task deps:download - name: ๐Ÿ”จ Setup Build tools - run: task install:goreleaser + run: task go:install:goreleaser - name: ๐Ÿ—๏ธ Build run: task build @@ -155,7 +90,7 @@ jobs: run: task deps:download - name: ๐Ÿ”จ Setup Test tools - run: task test:tools + run: task go:tools:test - name: ๐Ÿงช Run Tests run: task testunit diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 1e51e75..a4928b6 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -27,7 +27,4 @@ jobs: - name: ๐Ÿ•ต๏ธ Run Dependency Review uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4.8.2 with: - vulnerability-check: true - license-check: true - show-openssf-scorecard: true - comment-summary-in-pr: always + comment-summary-in-pr: on-failure diff --git a/.github/workflows/e2e-tests.yaml b/.github/workflows/e2e-tests.yaml index 5c7194c..65c1ce2 100644 --- a/.github/workflows/e2e-tests.yaml +++ b/.github/workflows/e2e-tests.yaml @@ -8,12 +8,15 @@ on: pull_request: merge_group: workflow_dispatch: - schedule: - cron: 0 21 * * * permissions: {} +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + jobs: e2e-tests: if: github.event_name != 'pull_request' @@ -29,6 +32,9 @@ jobs: contents: read id-token: write steps: + - name: ๐Ÿฉบ Debug + uses: raven-actions/debug@9dbdeb7eea607a7d73411895c65987e71d59a466 # v1.2.0 + - name: โคต๏ธ Checkout uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: @@ -48,18 +54,40 @@ jobs: run: task deps:download - name: ๐Ÿ”จ Setup Test tools - run: task test:tools + run: task go:tools:test - name: ๐Ÿ“ฆ Install Bicep if: matrix.type == 'bicep' - run: task install:bicep + run: task az:install:bicep - - name: ๐Ÿšง Setup Terraform + - name: ๐Ÿ“ฆ Install Terraform if: matrix.type == 'terraform' - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 - with: - terraform_wrapper: false - terraform_version: 1.10.4 + run: task tf:install:terraform + + - name: ๐ŸŒŒ Ensure bin path + if: runner.os == 'Linux' + run: | + INSTALL_PATH="$HOME/.local/bin" + + echo "$INSTALL_PATH" >> "${GITHUB_PATH}" + export PATH="$INSTALL_PATH:$PATH" + shell: bash + + - name: ๐ŸŒŒ Ensure bin path + if: runner.os == 'Windows' + run: | + $INSTALL_PATH = "$env:USERPROFILE\.local\bin" + $INSTALL_PATH | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + $env:PATH = "$INSTALL_PATH;$env:PATH" + + $INSTALL_PATH = "$env:LOCALAPPDATA\Microsoft\WinGet\Links" + $INSTALL_PATH | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + $env:PATH = "$INSTALL_PATH;$env:PATH" + + $INSTALL_PATH = "$env:LOCALAPPDATA\Microsoft\WindowsApps" + $INSTALL_PATH | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + $env:PATH = "$INSTALL_PATH;$env:PATH" + shell: pwsh - name: ๐Ÿ” Azure Login uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0 @@ -92,6 +120,7 @@ jobs: name: ๐Ÿงช Check E2E Tests needs: e2e-tests runs-on: ubuntu-24.04 + timeout-minutes: 5 steps: - name: โœ… OK if: ${{ !(contains(needs.*.result, 'failure')) }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..e4098e9 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,119 @@ +# yaml-language-server: $schema=https://www.schemastore.org/github-workflow.json +--- +name: ๐Ÿงน Lint + +on: + push: + branches: + - main + pull_request: + branches: + - main + types: + - opened + - reopened + - synchronize + - ready_for_review + merge_group: + +permissions: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + lint: + name: ๐Ÿงน Lint + runs-on: ubuntu-24.04 + timeout-minutes: 15 + permissions: + contents: read + strategy: + fail-fast: false + matrix: + task: + - gh + - sh + - go + - md + - yml + steps: + - name: ๐Ÿฉบ Debug + uses: raven-actions/debug@9dbdeb7eea607a7d73411895c65987e71d59a466 # v1.2.0 + + - name: โคต๏ธ Checkout + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + + - name: ๐Ÿšง Setup Node + uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 + with: + node-version-file: .node-version + + - name: ๐Ÿšง Setup Go + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 + with: + go-version-file: go.mod + + - name: ๐Ÿšง Setup Task + uses: go-task/setup-task@0ab1b2a65bc55236a3bc64cde78f80e20e8885c2 # v1.0.0 + with: + repo-token: ${{ github.token }} + + - name: ๐Ÿšง Setup runtime (UV) + run: task runtime:setup:uv + env: + GITHUB_TOKEN: ${{ github.token }} + + - name: ๐Ÿ”จ Setup tools + run: task ${{ matrix.task }}:tools:lint + env: + GITHUB_TOKEN: ${{ github.token }} + + - name: ๐ŸŒŒ Ensure bin path + if: runner.os == 'Linux' + run: | + INSTALL_PATH="$HOME/.local/bin" + + echo "$INSTALL_PATH" >> "${GITHUB_PATH}" + export PATH="$INSTALL_PATH:$PATH" + shell: bash + + - name: ๐ŸŒŒ Ensure bin path + if: runner.os == 'Windows' + run: | + $INSTALL_PATH = "$env:USERPROFILE\.local\bin" + $INSTALL_PATH | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + $env:PATH = "$INSTALL_PATH;$env:PATH" + + $INSTALL_PATH = "$env:LOCALAPPDATA\Microsoft\WinGet\Links" + $INSTALL_PATH | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + $env:PATH = "$INSTALL_PATH;$env:PATH" + + $INSTALL_PATH = "$env:LOCALAPPDATA\Microsoft\WindowsApps" + $INSTALL_PATH | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + $env:PATH = "$INSTALL_PATH;$env:PATH" + shell: pwsh + + - name: โœ”๏ธ Run lint (${{ matrix.task }}) + run: task ${{ matrix.task }}:lint + + - name: ๐Ÿ”€ Check for differences + if: github.event.pull_request.user.login != 'dependabot[bot]' + run: task diff -- "${{ matrix.task }}:lint" + + check-lint: + if: always() + name: ๐Ÿงน Check Lint + needs: lint + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - name: โœ… OK + if: ${{ !(contains(needs.*.result, 'failure')) }} + run: exit 0 + - name: ๐Ÿ›‘ Failure + if: ${{ contains(needs.*.result, 'failure') }} + run: exit 1 diff --git a/.goreleaser.yml b/.goreleaser.yml index 38f40b8..907f650 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -3,6 +3,10 @@ --- version: 2 +project_name: azmpf + +report_sizes: true + before: hooks: - go mod download @@ -11,8 +15,11 @@ archives: - files: - src: LICENSE dst: LICENSE.txt - formats: [zip] - name_template: "az{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" + format_overrides: + - goos: windows + formats: + - zip + name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}" builds: - dir: cmd @@ -22,7 +29,7 @@ builds: flags: - -trimpath ldflags: - - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{ .CommitDate }} + - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{ .Date }} goos: - windows - linux @@ -35,24 +42,28 @@ builds: ignore: - goos: darwin goarch: "386" - binary: "az{{ .ProjectName }}_v{{ .Version }}" + binary: "{{ .ProjectName }}" + +sboms: + - id: default + disable: false checksum: algorithm: sha256 - name_template: "az{{ .ProjectName }}_{{ .Version }}_SHA256SUMS" + name_template: "{{ .ProjectName }}_SHA256SUMS" signs: - artifacts: checksum args: # if you are using this in a GitHub action or some other automated pipeline, you # need to pass the batch flag to indicate its not interactive. - - "--batch" - - "--local-user" + - --batch + - --local-user - "{{ .Env.GPG_FINGERPRINT }}" # set this environment variable for your signing key - - "--output" - - "${signature}" - - "--detach-sign" - - "${artifact}" + - --output + - ${signature} + - --detach-sign + - ${artifact} release: # If you want to manually examine the release before its live, uncomment this line: diff --git a/.node-version b/.node-version new file mode 100644 index 0000000..a45fd52 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +24 diff --git a/.taskfiles/_internal.Taskfile.yml b/.taskfiles/_internal.Taskfile.yml new file mode 100644 index 0000000..6f74ec0 --- /dev/null +++ b/.taskfiles/_internal.Taskfile.yml @@ -0,0 +1,145 @@ +# yaml-language-server: $schema=https://taskfile.dev/schema.json +# docs: https://taskfile.dev +--- +version: "3" + +vars: + PWSH: pwsh -NonInteractive -NoProfile -NoLogo -Command + PWSH_SCRIPT: pwsh -NonInteractive -NoProfile -NoLogo -ExecutionPolicy Bypass -File + APT_UPDATE: sudo apt-get -o DPkg::Lock::Timeout=-1 update + APT_INSTALL: sudo apt-get -o DPkg::Lock::Timeout=-1 install -y + PYTHON_VERSION: + sh: | + [ -f .python-version ] && cat .python-version || echo "3.14" + NODE_VERSION: + sh: | + [ -f .node-version ] && cat .node-version || echo "24" + +env: + DEBIAN_FRONTEND: noninteractive + +tasks: + command: + desc: Check if command exists (internal) + silent: true + run: once + preconditions: + - sh: | + {{empty .CLI_ARGS | not}} + msg: No command provided. Please provide a command to check. + cmds: + - cmd: command -v "{{.CLI_ARGS}}" + platforms: [linux, darwin] + - cmd: | + {{.PWSH}} 'Get-Command "{{.CLI_ARGS}}"' + platforms: [windows] + + command:version: + desc: Check if desired version of command is installed (internal) + silent: true + run: once + preconditions: + - sh: | + {{empty .CLI_ARGS | not}} + msg: No command and version provided. Please provide a command and version to check. + vars: + CMD: "{{index .CLI_ARGS_LIST 0}}" + VER: "{{index .CLI_ARGS_LIST 1}}" + cmds: + - cmd: | + {{if ne .VER "latest"}} + {{.CMD}} | grep -F "{{.VER}}" + {{end}} + platforms: [linux, darwin] + - cmd: | + {{if ne .VER "latest"}} + {{.PWSH}} ' + $match = (& {{.CMD}}) | Select-String -Pattern "{{.VER}}" -SimpleMatch + if (-not $match) { exit 1 } + ' + {{end}} + platforms: [windows] + + _install:go: + desc: go install + internal: true + preconditions: + - sh: task internal:command -- go + msg: "go is not installed. Please install go: https://go.dev/doc/install" + requires: + vars: [APP] + cmds: + - go install {{.APP}} + + _install:winget: + desc: winget install + internal: true + preconditions: + - sh: task internal:command -- winget + msg: "winget is not installed. Please install winget: https://learn.microsoft.com/windows/package-manager/winget/" + requires: + vars: [APP] + vars: + # yamllint disable-line rule:quoted-strings + VERSION: '{{if and .VERSION (ne .VERSION "latest")}}--version {{.VERSION}}{{end}}' + cmds: + - winget install --id "{{.APP}}" --accept-source-agreements --accept-package-agreements --disable-interactivity {{.VERSION}} + ignore_error: true + platforms: [windows] + + _install:brew: + desc: brew install + internal: true + preconditions: + - sh: task internal:command -- brew + msg: brew is not installed. Please install brew. + requires: + vars: [APP] + cmds: + - brew install {{.APP}} + platforms: [darwin] + + _install:brew:tap: + desc: brew tap + internal: true + preconditions: + - sh: task internal:command -- brew + msg: brew is not installed. Please install brew. + requires: + vars: [APP] + cmds: + - brew tap {{.APP}} + platforms: [darwin] + + _install:pipx: + desc: pipx install + internal: true + preconditions: + - sh: task internal:command -- pipx + msg: "pipx is not installed. Please install pipx: https://pipx.pypa.io/" + requires: + vars: [APP] + cmds: + - pipx install --include-deps {{.APP}} + + _install:npm: + desc: npm install + internal: true + preconditions: + - sh: task internal:command -- npm + msg: "npm is not installed. Please install npm: https://docs.npmjs.com/cli/commands/npm" + requires: + vars: [APP] + cmds: + - npm install {{if .OPTS}}{{.OPTS}}{{else}}--global{{end}} {{.APP}} + + _install:uv: + desc: uv install + internal: true + preconditions: + - sh: task internal:command -- uv + msg: uv is not installed. Please install by running 'task runtime:setup:uv' + requires: + vars: [APP] + cmds: + - uv tool install --force --upgrade {{.APP}} diff --git a/.taskfiles/azure.Taskfile.yml b/.taskfiles/azure.Taskfile.yml new file mode 100644 index 0000000..08c8b8c --- /dev/null +++ b/.taskfiles/azure.Taskfile.yml @@ -0,0 +1,79 @@ +# yaml-language-server: $schema=https://taskfile.dev/schema.json +# docs: https://taskfile.dev +--- +version: "3" + +vars: + AZURE_VERSION: + map: + az: latest + azd: latest + bicep: latest + +tasks: + # * Install Azure CLI + install:az: + desc: Install Azure CLI + cmds: + - task: :internal:_install:winget + vars: + APP: Microsoft.AzureCLI + VERSION: "{{.AZURE_VERSION.az}}" + - task: :internal:_install:brew + vars: + APP: azure-cli@{{.AZURE_VERSION.az}} + - cmd: | + sudo ./scripts/install_az.sh "{{.AZURE_VERSION.az}}" + platforms: [linux] + dir: "{{.TASKFILE_DIR}}" + + + # * Install Azure Developer CLI + install:azd: + desc: Install Azure Developer CLI + cmds: + - task: :internal:_install:winget + vars: + APP: Microsoft.Azd + VERSION: "{{.AZURE_VERSION.azd}}" + - task: :internal:_install:brew:tap + vars: + APP: azure/azd + - task: :internal:_install:brew + vars: + APP: azd@{{.AZURE_VERSION.azd}} + - cmd: | + ./scripts/install_azd.sh "{{.AZURE_VERSION.azd}}" + platforms: [linux] + dir: "{{.TASKFILE_DIR}}" + + # * Install Bicep + install:bicep: + desc: Install Bicep + cmds: + - task: :internal:_install:winget + vars: + APP: Microsoft.Bicep + VERSION: "{{.AZURE_VERSION.bicep}}" + - task: :internal:_install:brew:tap + vars: + APP: azure/bicep + - task: :internal:_install:brew + vars: + APP: bicep@{{.AZURE_VERSION.bicep}} + - cmd: | + ./scripts/install_bicep.sh "{{.AZURE_VERSION.bicep}}" + platforms: [linux] + dir: "{{.TASKFILE_DIR}}" + + # * Tools + tools: + desc: Install Azure tools + vars: + ITEMS: + - az + - azd + - bicep + cmds: + - for: { var: ITEMS } + task: install:{{.ITEM}} diff --git a/.taskfiles/github.Taskfile.yml b/.taskfiles/github.Taskfile.yml new file mode 100644 index 0000000..94545b6 --- /dev/null +++ b/.taskfiles/github.Taskfile.yml @@ -0,0 +1,200 @@ +# yaml-language-server: $schema=https://taskfile.dev/schema.json +# docs: https://taskfile.dev +--- +version: "3" + +vars: + GITHUB_VERSION: + map: + actionlint: latest + act: latest + pinact: latest + ghalint: latest + gh: latest + +tasks: + # * Install Actionlint + install:actionlint: + desc: Install Actionlint + cmds: + - task: :internal:_install:winget + vars: + APP: rhysd.actionlint + VERSION: "{{.GITHUB_VERSION.actionlint}}" + - task: :internal:_install:brew + vars: + APP: actionlint@{{.GITHUB_VERSION.actionlint}} + - cmd: | + ./scripts/install_actionlint.sh "{{.GITHUB_VERSION.actionlint}}" + platforms: [linux] + dir: "{{.TASKFILE_DIR}}" + + # * Install ACT + install:act: + desc: Install ACT + cmds: + - task: :internal:_install:winget + vars: + APP: nektos.act + VERSION: "{{.GITHUB_VERSION.act}}" + - task: :internal:_install:brew + vars: + APP: act@{{.GITHUB_VERSION.act}} + - cmd: | + ./scripts/install_act.sh "{{.GITHUB_VERSION.act}}" + platforms: [linux] + dir: "{{.TASKFILE_DIR}}" + + # * Install PinAct + install:pinact: + desc: Install PinAct + summary: Install a pinned version of Act + cmds: + - cmd: | + {{.PWSH_SCRIPT}} ./scripts/install_pinact.ps1 "{{.GITHUB_VERSION.pinact}}" + platforms: [windows] + - task: :internal:_install:brew + vars: + APP: pinact@{{.GITHUB_VERSION.pinact}} + - cmd: | + ./scripts/install_pinact.sh "{{.GITHUB_VERSION.pinact}}" + platforms: [linux] + dir: "{{.TASKFILE_DIR}}" + + # * Install GHAlint + install:ghalint: + desc: Install GHAlint + cmds: + - cmd: | + {{.PWSH_SCRIPT}} ./scripts/install_ghalint.ps1 "{{.GITHUB_VERSION.ghalint}}" + platforms: [windows] + - task: :internal:_install:brew + vars: + APP: ghalint@{{.GITHUB_VERSION.ghalint}} + - cmd: | + ./scripts/install_ghalint.sh "{{.GITHUB_VERSION.ghalint}}" + platforms: [linux] + dir: "{{.TASKFILE_DIR}}" + + # * Install GitHub CLI + install:gh: + desc: Install GitHub CLI + cmds: + - task: :internal:_install:winget + vars: + APP: GitHub.cli + VERSION: "{{.GITHUB_VERSION.gh}}" + - task: :internal:_install:brew + vars: + APP: gh@{{.GITHUB_VERSION.gh}} + - cmd: | + sudo ./scripts/install_gh.sh "{{.GITHUB_VERSION.gh}}" + platforms: [linux] + dir: "{{.TASKFILE_DIR}}" + + # * Tools + tools: + desc: Install GitHub tools + vars: + ITEMS: + - act + - gh + cmds: + - for: { var: ITEMS } + task: install:{{.ITEM}} + - task: tools:lint + + # * Tools (lint) + tools:lint: + desc: Install GitHub tools (lint) + vars: + ITEMS: + - actionlint + - pinact + - ghalint + cmds: + - for: { var: ITEMS } + task: install:{{.ITEM}} + - task sh:tools:lint + + # * PinAct Runner + _run:pinact: + internal: true + desc: Run PinAct + preconditions: + - sh: task internal:command -- pinact + msg: "โŒ Error: pinact is not installed. Please run 'task gh:install:pinact'" + - sh: task internal:command:version -- "pinact version" "{{.GITHUB_VERSION.pinact}}" + msg: "โŒ Version Mismatch: You are not using {{.GITHUB_VERSION.pinact}} version. Please run 'task gh:install:pinact'" + sources: + - .github/workflows/*.yml + - .github/workflows/*.yaml + - "**/action.yml" + - "**/action.yaml" + vars: + # yamllint disable-line rule:quoted-strings + MODE: '{{default "--check --diff" .MODE}}' + cmds: + - defer: | + {{if and .EXIT_CODE (eq .MODE "--check")}}echo "โš ๏ธ GitHub Workflows/Actions files are not formatted correctly. Please run 'task gh:run:pinact:fix' to fix formatting issues."{{end}} + - pinact run {{.MODE}} + dir: "{{.ROOT_DIR}}" + + # * Run PinAct Check + run:pinact:check: + desc: Run PinAct Check + cmds: + - task: _run:pinact + vars: { MODE: --check --diff } + + # * Run PinAct Fix + run:pinact:fix: + desc: Run PinAct fix + cmds: + - task: _run:pinact + vars: { MODE: --fix --diff } + + # * Run Actionlint + run:actionlint: + desc: Run Actionlint + preconditions: + - sh: task internal:command -- actionlint + msg: "โŒ Error: actionlint is not installed. Please run 'task gh:install:actionlint'" + - sh: task internal:command:version -- "actionlint --version" "{{.GITHUB_VERSION.actionlint}}" + msg: "โŒ Version Mismatch: You are not using {{.GITHUB_VERSION.actionlint}} version. Please run 'task gh:install:actionlint'" + sources: + - .github/workflows/*.yml + - .github/workflows/*.yaml + - "**/action.yml" + - "**/action.yaml" + cmds: + - actionlint -config-file ./.github/linters/actionlint.yaml + dir: "{{.ROOT_DIR}}" + + # * Run GHALint + run:ghalint: + desc: Run GHALint + preconditions: + - sh: task internal:command -- ghalint + msg: "โŒ Error: ghalint is not installed. Please run 'task gh:install:ghalint'" + - sh: task internal:command:version -- "ghalint --version" "{{.GITHUB_VERSION.ghalint}}" + msg: "โŒ Version Mismatch: You are not using {{.GITHUB_VERSION.ghalint}} version. Please run 'task gh:install:ghalint'" + sources: + - .github/workflows/*.yml + - .github/workflows/*.yaml + - "**/action.yml" + - "**/action.yaml" + env: + GHALINT_LOG_COLOR: always + cmds: + - ghalint run + - ghalint run-action + dir: "{{.ROOT_DIR}}" + + # * Lint + lint: + desc: Lint YAML files + cmds: + - task: run:ghalint + - task: run:actionlint + - task: run:pinact:fix diff --git a/.taskfiles/golang.Taskfile.yml b/.taskfiles/golang.Taskfile.yml new file mode 100644 index 0000000..d18280a --- /dev/null +++ b/.taskfiles/golang.Taskfile.yml @@ -0,0 +1,193 @@ +# yaml-language-server: $schema=https://taskfile.dev/schema.json +# docs: https://taskfile.dev +--- +version: "3" + +vars: + GO_VERSION: + map: + dlv: latest + deadcode: latest + golangciLint: latest + gotestsum: latest + govulncheck: latest + goimports: latest + gofumpt: latest + goTestCoverage: latest + gocoverCobertura: latest + goreleaser: latest + +tasks: + # * Install GoReleaser + install:goreleaser: + desc: Install GoReleaser + cmds: + - task: :internal:_install:winget + vars: + APP: goreleaser.goreleaser + VERSION: "{{.GO_VERSION.goreleaser}}" + - task: :internal:_install:brew + vars: + APP: goreleaser@{{.GO_VERSION.goreleaser}} + - cmd: | + ./scripts/install_goreleaser.sh "{{.GO_VERSION.goreleaser}}" + platforms: [linux] + dir: "{{.TASKFILE_DIR}}" + + # * Install Dlv + install:dlv: + desc: Install Dlv + cmds: + - task: :internal:_install:go + vars: + APP: github.com/go-delve/delve/cmd/dlv@{{.GO_VERSION.dlv}} + + # * Install DeadCode + install:deadcode: + desc: Install DeadCode + cmds: + - task: :internal:_install:go + vars: + APP: golang.org/x/tools/cmd/deadcode@{{.GO_VERSION.deadcode}} + + # * Install GolangCI-Lint + install:golangci-lint: + desc: Install GolangCI-Lint + cmds: + - task: :internal:_install:winget + vars: + APP: GolangCI.golangci-lint + VERSION: "{{.GO_VERSION.golangciLint}}" + - task: :internal:_install:brew + vars: + APP: golangci-lint@{{.GO_VERSION.golangciLint}} + - cmd: | + ./scripts/install_golangci-lint.sh "{{.GO_VERSION.golangciLint}}" + platforms: [linux] + dir: "{{.TASKFILE_DIR}}" + + # * Install GoTestSum + install:gotestsum: + desc: Install GoTestSum + cmds: + - task: :internal:_install:go + vars: + APP: gotest.tools/gotestsum@{{.GO_VERSION.gotestsum}} + + # * Install GoVulnCheck + install:govulncheck: + desc: Install GoVulnCheck + cmds: + - task: :internal:_install:go + vars: + APP: golang.org/x/vuln/cmd/govulncheck@{{.GO_VERSION.govulncheck}} + + # * Install GoImports + install:goimports: + desc: Install GoImports + cmds: + - task: :internal:_install:go + vars: + APP: golang.org/x/tools/cmd/goimports@{{.GO_VERSION.goimports}} + + # * Install GoFumpt + install:gofumpt: + desc: Install GoFumpt + cmds: + - task: :internal:_install:go + vars: + APP: mvdan.cc/gofumpt@{{.GO_VERSION.gofumpt}} + + # * Install Go Test Coverage + install:go-test-coverage: + desc: Install Go Test Coverage + cmds: + - task: :internal:_install:go + vars: + APP: github.com/vladopajic/go-test-coverage/v2@{{.GO_VERSION.goTestCoverage}} + + # * Install GoCover-Cobertura + install:gocover-cobertura: + desc: Install GoCover-Cobertura + cmds: + - task: :internal:_install:go + vars: + APP: github.com/boumenot/gocover-cobertura@{{.GO_VERSION.gocoverCobertura}} + + # * Tools + tools: + desc: Install Go tools + vars: + ITEMS: + - dlv + - deadcode + - golangci-lint + - gotestsum + - govulncheck + - goimports + - gofumpt + - go-test-coverage + - gocover-cobertura + - goreleaser + cmds: + - for: { var: ITEMS } + task: install:{{.ITEM}} + + # * Tools (test) + tools:test: + desc: Install Go tools (test) + vars: + ITEMS: + - gotestsum + - go-test-coverage + - gocover-cobertura + - goreleaser + cmds: + - for: { var: ITEMS } + task: install:{{.ITEM}} + + # * Tools (lint) + tools:lint: + desc: Install Go tools (lint) + vars: + ITEMS: + - golangci-lint + - govulncheck + cmds: + - for: { var: ITEMS } + task: install:{{.ITEM}} + + # * Run GoVulnCheck + run:govulncheck: + desc: Run govulncheck + preconditions: + - sh: task internal:command -- govulncheck + msg: "โŒ Error: govulncheck is not installed. Please run 'task go:install:govulncheck'" + - sh: task internal:command:version -- "govulncheck --version" "{{.GO_VERSION.govulncheck}}" + msg: "โŒ Version Mismatch: You are not using {{.GO_VERSION.govulncheck}} version. Please run 'task go:install:govulncheck'" + cmds: + - govulncheck -test -show verbose ./... + + # * Run GolangCI-Lint + run:golangci-lint: + desc: Run GolangCI-Lint + preconditions: + - sh: task internal:command -- golangci-lint + msg: "โŒ Error: golangci-lint is not installed. Please run 'task go:install:golangci-lint'" + - sh: task internal:command:version -- "golangci-lint --version" "{{.GO_VERSION.golangciLint}}" + msg: "โŒ Version Mismatch: You are not using {{.GO_VERSION.golangciLint}} version. Please run 'task go:install:golangci-lint'" + sources: + - "**/*.go" + - "**/*.mod" + - "**/*.sum" + - "**/go.work" + cmds: + - golangci-lint run --fix + dir: "{{.ROOT_DIR}}" + + # * Lint + lint: + desc: Lint Go files + cmds: + - task: run:golangci-lint + - task: run:govulncheck diff --git a/.taskfiles/markdown.Taskfile.yml b/.taskfiles/markdown.Taskfile.yml new file mode 100644 index 0000000..c41463a --- /dev/null +++ b/.taskfiles/markdown.Taskfile.yml @@ -0,0 +1,124 @@ +# yaml-language-server: $schema=https://taskfile.dev/schema.json +# docs: https://taskfile.dev +--- +version: "3" + +vars: + MARKDOWN_VERSION: + map: + markdownlintCli2: latest + markdownTableFormatter: latest + +tasks: + # * Install markdownlint-cli2 + install:markdownlint-cli2: + desc: Install markdownlint-cli2 + cmds: + - task: :internal:_install:npm + vars: + APP: markdownlint-cli2@{{.MARKDOWN_VERSION.markdownlintCli2}} + + # * Install markdown-table-formatter + install:markdown-table-formatter: + desc: Install markdown-table-formatter + cmds: + - task: :internal:_install:npm + vars: + APP: markdown-table-formatter@{{.MARKDOWN_VERSION.markdownTableFormatter}} + + # * Tools + tools:lint: + desc: Install Markdown tools + aliases: + - tools + vars: + ITEMS: + - markdownlint-cli2 + - markdown-table-formatter + cmds: + - for: { var: ITEMS } + task: install:{{.ITEM}} + + # * Run markdownlint-cli2 + _run:markdownlint-cli2: + internal: true + desc: Run markdownlint-cli2 + preconditions: + - sh: task internal:command -- markdownlint-cli2 + msg: "โŒ Error: markdownlint-cli2 is not installed. Please run 'task md:install:markdownlint-cli2'" + - sh: task internal:command:version -- "markdownlint-cli2 --help" "{{.MARKDOWN_VERSION.markdownlintCli2}}" + msg: "โŒ Version Mismatch: You are not using {{.MARKDOWN_VERSION.markdownlintCli2}} version. Please run 'task md:install:markdownlint-cli2'" + sources: + - "**/*.md" + vars: + # yamllint disable-line rule:quoted-strings + MODE: '{{default "" .MODE}}' + cmds: + - defer: | + {{if and .EXIT_CODE (eq .MODE "")}}echo "โš ๏ธ Markdown files are not formatted correctly. Please run 'task md:run:markdownlint-cli2:fix' to fix formatting issues."{{end}} + - markdownlint-cli2 "./**/*.md" --config "./.github/linters/.markdownlint-cli2.yaml" {{.MODE}} + dir: "{{.ROOT_DIR}}" + + # * Run markdownlint-cli2 Check + run:markdownlint-cli2:check: + desc: Run markdownlint-cli2 (check) + cmds: + - task: _run:markdownlint-cli2 + vars: { MODE: "" } + + # * Run markdownlint-cli2 Fix + run:markdownlint-cli2:fix: + desc: Run markdownlint-cli2 (fix) + cmds: + - task: _run:markdownlint-cli2 + vars: { MODE: --fix } + + # * Run markdown-table-formatter + _run:markdown-table-formatter: + internal: true + desc: Run markdown-table-formatter + preconditions: + - sh: task internal:command -- markdown-table-formatter + msg: "โŒ Error: markdown-table-formatter is not installed. Please run 'task md:install:markdown-table-formatter'" + - sh: task internal:command:version -- "markdown-table-formatter --version" "{{.MARKDOWN_VERSION.markdownTableFormatter}}" + msg: "โŒ Version Mismatch: You are not using {{.MARKDOWN_VERSION.markdownTableFormatter}} version. Please run 'task md:install:markdown-table-formatter'" + sources: + - "**/*.md" + vars: + # yamllint disable-line rule:quoted-strings + MODE: '{{default "--check" .MODE}}' + cmds: + - defer: | + {{if and .EXIT_CODE (eq .MODE "--check")}}echo "โš ๏ธ Markdown tables are not formatted correctly. Please run 'task md:run:markdown-table-formatter:fix' to fix formatting issues."{{end}} + - markdown-table-formatter "./**/*.md" {{.MODE}} + dir: "{{.ROOT_DIR}}" + + # * Run markdown-table-formatter Check + run:markdown-table-formatter:check: + desc: Run markdown-table-formatter (check) + cmds: + - task: _run:markdown-table-formatter + vars: { MODE: --check } + + # * Run markdown-table-formatter Fix + run:markdown-table-formatter:fix: + desc: Run markdown-table-formatter (fix) + cmds: + - task: _run:markdown-table-formatter + vars: { MODE: "" } + + # * Lint Check + lint:check: + desc: Lint Markdown files (check) + cmds: + - task: run:markdownlint-cli2:check + - task: run:markdown-table-formatter:check + + # * Lint Fix + lint:fix: + aliases: + - lint + desc: Lint Markdown files (fix) + cmds: + - task: run:markdownlint-cli2:fix + - task: run:markdown-table-formatter:fix diff --git a/.taskfiles/runtime.Taskfile.yml b/.taskfiles/runtime.Taskfile.yml new file mode 100644 index 0000000..2be7c2b --- /dev/null +++ b/.taskfiles/runtime.Taskfile.yml @@ -0,0 +1,86 @@ +# yaml-language-server: $schema=https://taskfile.dev/schema.json +# docs: https://taskfile.dev +--- +version: "3" + +vars: + RUNTIME_VERSION: + map: + uv: latest + fnm: latest + pwsh: latest + golang: latest + +tasks: + setup:uv: + desc: uv setup + cmds: + - cmd: | + ./scripts/setup_uv.sh "{{.RUNTIME_VERSION.uv}}" + platforms: [linux] + - task: :internal:_install:brew + vars: + APP: uv@{{.RUNTIME_VERSION.uv}} + - task: :internal:_install:winget + vars: + APP: astral-sh.uv + VERSION: "{{.RUNTIME_VERSION.uv}}" + dir: "{{.TASKFILE_DIR}}" + + setup:fnm: + desc: fnm setup + cmds: + - cmd: | + ./scripts/setup_fnm.sh "{{.RUNTIME_VERSION.fnm}}" + platforms: [linux] + - task: :internal:_install:brew + vars: + APP: fnm@{{.RUNTIME_VERSION.fnm}} + - task: :internal:_install:winget + vars: + APP: Schniz.fnm + VERSION: "{{.RUNTIME_VERSION.fnm}}" + dir: "{{.TASKFILE_DIR}}" + + setup:pwsh: + desc: pwsh setup + cmds: + - cmd: | + sudo ./scripts/setup_pwsh.sh "{{.RUNTIME_VERSION.pwsh}}" + platforms: [linux] + - cmd: | + ./scripts/setup_pwsh.sh "{{.RUNTIME_VERSION.pwsh}}" + platforms: [darwin] + - task: :internal:_install:winget + vars: + APP: Microsoft.PowerShell + VERSION: "{{.RUNTIME_VERSION.pwsh}}" + dir: "{{.TASKFILE_DIR}}" + + setup:golang: + desc: golang setup + cmds: + - cmd: | + ./scripts/setup_golang.sh "{{.RUNTIME_VERSION.golang}}" + platforms: [linux] + - task: :internal:_install:brew + vars: + APP: golang@{{.RUNTIME_VERSION.golang}} + - task: :internal:_install:winget + vars: + APP: GoLang.Go + VERSION: "{{.RUNTIME_VERSION.golang}}" + dir: "{{.TASKFILE_DIR}}" + + # * Setup + setup: + desc: Setup Runtime environments + vars: + ITEMS: + - golang + - uv + - fnm + - pwsh + cmds: + - for: { var: ITEMS } + task: setup:{{.ITEM}} diff --git a/.taskfiles/scripts/install_act.sh b/.taskfiles/scripts/install_act.sh new file mode 100755 index 0000000..14187b9 --- /dev/null +++ b/.taskfiles/scripts/install_act.sh @@ -0,0 +1,96 @@ +#!/bin/bash + +set -euo pipefail + +# Constants +readonly GITHUB_OWNER="nektos" +readonly GITHUB_REPO="act" +readonly TOOL_NAME="act" +readonly INSTALL_SCRIPT_URL="https://raw.githubusercontent.com/${GITHUB_OWNER}/${GITHUB_REPO}/master/install.sh" + +# Configuration (can be overridden by env) +VERSION="${1:-${VERSION:-latest}}" +INSTALL_DIR="${2:-${INSTALL_DIR:-}}" + +# Logging helper +log() { + echo "-> $*" >&2 +} + +# Error handling helper +die() { + echo "X Error: $*" >&2 + exit "${2:-1}" +} + +# Help message +usage() { + cat < "latest", numeric -> add "v" prefix +if [[ -z "${VERSION//[[:space:]]/}" ]]; then + VERSION="latest" +elif [[ "${VERSION}" != "latest" && "${VERSION}" =~ ^[0-9] ]]; then + VERSION="v${VERSION}" +fi + +# Determine install directory +if [[ -z "${INSTALL_DIR}" ]]; then + if [[ "${EUID}" -eq 0 ]]; then + INSTALL_DIR="/usr/local/bin" + else + INSTALL_DIR="${HOME}/.local/bin" + fi +fi + +# Check dependencies +command -v curl >/dev/null 2>&1 || die "Missing required dependency: curl" + +# Create install directory if it doesn't exist +if [[ ! -d "${INSTALL_DIR}" ]]; then + mkdir -p "${INSTALL_DIR}" || die "Cannot create install directory ${INSTALL_DIR}" +fi + +log "Installing ${TOOL_NAME} (${VERSION}) to ${INSTALL_DIR}" + +# GitHub API authentication +if [[ -n "${GITHUB_TOKEN:-}" ]]; then + ghAuthHeader=(-H "Authorization: Bearer ${GITHUB_TOKEN}") +else + ghAuthHeader=() +fi + +# Execute remote installation script +log "Fetching and executing official installation script" +if ! curl "${ghAuthHeader[@]}" -fsSL "${INSTALL_SCRIPT_URL}" | /bin/bash -s -- -b "${INSTALL_DIR}" "${VERSION}"; then + die "Installation failed. Check version or network connection." +fi + +log "โœ“ Successfully installed ${TOOL_NAME} to ${INSTALL_DIR}/${TOOL_NAME}" + +# Run tool version to verify +"${INSTALL_DIR}/${TOOL_NAME}" --version || die "Installed binary failed to run (${INSTALL_DIR}/${TOOL_NAME})" diff --git a/.taskfiles/scripts/install_actionlint.sh b/.taskfiles/scripts/install_actionlint.sh new file mode 100755 index 0000000..4d1bf76 --- /dev/null +++ b/.taskfiles/scripts/install_actionlint.sh @@ -0,0 +1,96 @@ +#!/bin/bash + +set -euo pipefail + +# Constants +readonly GITHUB_OWNER="rhysd" +readonly GITHUB_REPO="actionlint" +readonly TOOL_NAME="actionlint" +readonly INSTALL_SCRIPT_URL="https://raw.githubusercontent.com/${GITHUB_OWNER}/${GITHUB_REPO}/main/scripts/download-actionlint.bash" + +# Configuration (can be overridden by env) +VERSION="${1:-${VERSION:-latest}}" +INSTALL_DIR="${2:-${INSTALL_DIR:-}}" + +# Logging helper +log() { + echo "-> $*" >&2 +} + +# Error handling helper +die() { + echo "X Error: $*" >&2 + exit "${2:-1}" +} + +# Help message +usage() { + cat < "latest", numeric -> add "v" prefix +if [[ -z "${VERSION//[[:space:]]/}" ]]; then + VERSION="latest" +elif [[ "${VERSION}" != "latest" && "${VERSION}" =~ ^[0-9] ]]; then + VERSION="v${VERSION}" +fi + +# Determine install directory +if [[ -z "${INSTALL_DIR}" ]]; then + if [[ "${EUID}" -eq 0 ]]; then + INSTALL_DIR="/usr/local/bin" + else + INSTALL_DIR="${HOME}/.local/bin" + fi +fi + +# Check dependencies +command -v curl >/dev/null 2>&1 || die "Missing required dependency: curl" + +# Create install directory if it doesn't exist +if [[ ! -d "${INSTALL_DIR}" ]]; then + mkdir -p "${INSTALL_DIR}" || die "Cannot create install directory ${INSTALL_DIR}" +fi + +log "Installing ${TOOL_NAME} (${VERSION}) to ${INSTALL_DIR}" + +# GitHub API authentication +if [[ -n "${GITHUB_TOKEN:-}" ]]; then + ghAuthHeader=(-H "Authorization: Bearer ${GITHUB_TOKEN}") +else + ghAuthHeader=() +fi + +# Execute remote installation script +log "Fetching and executing official installation script" +if ! curl "${ghAuthHeader[@]}" -fsSL "${INSTALL_SCRIPT_URL}" | /bin/bash -s -- "${VERSION}" "${INSTALL_DIR}"; then + die "Installation failed. Check version or network connection." +fi + +log "โœ“ Successfully installed ${TOOL_NAME} to ${INSTALL_DIR}/${TOOL_NAME}" + +# Run tool version to verify +"${INSTALL_DIR}/${TOOL_NAME}" -version || die "Installed binary failed to run (${INSTALL_DIR}/${TOOL_NAME})" diff --git a/.taskfiles/scripts/install_az.sh b/.taskfiles/scripts/install_az.sh new file mode 100755 index 0000000..7312a52 --- /dev/null +++ b/.taskfiles/scripts/install_az.sh @@ -0,0 +1,114 @@ +#!/bin/bash + +set -euo pipefail + +# Constants +readonly TOOL_NAME="az" +readonly KEYRING_URL="https://packages.microsoft.com/keys/microsoft.asc" +readonly KEYRING_PATH="/etc/apt/keyrings/microsoft.gpg" +readonly SOURCES_LIST="/etc/apt/sources.list.d/azure-cli.sources" + +# Configuration (can be overridden by env) +VERSION="${1:-${VERSION:-latest}}" + +log() { + echo "-> $*" >&2 +} + +die() { + echo "X Error: $*" >&2 + exit "${2:-1}" +} + +usage() { + cat < "latest" +if [[ -z "${VERSION//[[:space:]]/}" ]]; then + VERSION="latest" +fi + +check_sudo + +log "Installing ${TOOL_NAME} (${VERSION}) via apt repository" + +# Check dependencies +log "Checking dependencies" +for dep in curl gpg lsb_release; do + command -v "${dep}" >/dev/null 2>&1 || die "Missing required dependency: ${dep}" +done + +# Install apt transport dependencies +log "Installing apt dependencies" +apt-get update || die "Failed to update apt cache" +apt-get install -y apt-transport-https ca-certificates gnupg || die "Failed to install apt dependencies" + +# Create keyrings directory +log "Setting up Microsoft GPG key" +# shellcheck disable=SC2174 +mkdir -p -m 755 /etc/apt/keyrings || die "Failed to create keyrings directory" + +# Download and install GPG key +if ! curl -sLS "${KEYRING_URL}" | gpg --batch --yes --dearmor -o "${KEYRING_PATH}"; then + die "Failed to download and install GPG keyring" +fi +chmod go+r "${KEYRING_PATH}" || die "Failed to set keyring permissions" + +# Add apt repository using DEB822 format +log "Adding Azure CLI apt repository" +arch="$(dpkg --print-architecture)" || die "Failed to detect architecture" +codename="$(lsb_release -cs)" || die "Failed to detect distribution codename" + +cat </dev/null || die "Failed to add apt repository" +Types: deb +URIs: https://packages.microsoft.com/repos/azure-cli/ +Suites: ${codename} +Components: main +Architectures: ${arch} +Signed-by: ${KEYRING_PATH} +EOF + +# Update apt cache and install +log "Updating apt cache" +apt-get update || die "Failed to update apt cache" + +log "Installing ${TOOL_NAME}" +if [[ "${VERSION}" == "latest" ]]; then + apt-get install -y azure-cli || die "Failed to install ${TOOL_NAME}" +else + # Install specific version (format: azure-cli=version-1~codename) + apt-get install -y "azure-cli=${VERSION}-1~${codename}" || die "Failed to install ${TOOL_NAME} version ${VERSION}" +fi + +log "โœ“ Successfully installed ${TOOL_NAME}" + +# Verify installation +"${TOOL_NAME}" --version || die "Installed binary failed to run" diff --git a/.taskfiles/scripts/install_azd.sh b/.taskfiles/scripts/install_azd.sh new file mode 100755 index 0000000..7d929fd --- /dev/null +++ b/.taskfiles/scripts/install_azd.sh @@ -0,0 +1,87 @@ +#!/bin/bash + +set -euo pipefail + +# Constants +readonly TOOL_NAME="azd" +readonly INSTALL_SCRIPT_URL="https://aka.ms/install-azd.sh" + +# Configuration (can be overridden by env) +VERSION="${1:-${VERSION:-latest}}" +INSTALL_DIR="${2:-${INSTALL_DIR:-}}" + +# Logging helper +log() { + echo "-> $*" >&2 +} + +# Error handling helper +die() { + echo "X Error: $*" >&2 + exit "${2:-1}" +} + +# Help message +usage() { + cat < "latest", numeric -> add "v" prefix +if [[ -z "${VERSION//[[:space:]]/}" ]]; then + VERSION="latest" +elif [[ "${VERSION}" != "latest" && "${VERSION}" =~ ^[0-9] ]]; then + VERSION="v${VERSION}" +fi + +# Determine install directory +if [[ -z "${INSTALL_DIR}" ]]; then + if [[ "${EUID}" -eq 0 ]]; then + INSTALL_DIR="/usr/local/bin" + else + INSTALL_DIR="${HOME}/.local/bin" + fi +fi + +# Check dependencies +command -v curl >/dev/null 2>&1 || die "Missing required dependency: curl" + +# Create install directory if it doesn't exist +if [[ ! -d "${INSTALL_DIR}" ]]; then + mkdir -p "${INSTALL_DIR}" || die "Cannot create install directory ${INSTALL_DIR}" +fi + +log "Installing ${TOOL_NAME} (${VERSION}) to ${INSTALL_DIR}" + +# Execute remote installation script +log "Fetching and executing official installation script" +if ! curl -fsSL "${INSTALL_SCRIPT_URL}" | /bin/bash -s -- --version "${VERSION}" --install-folder "${INSTALL_DIR}" --symlink-folder "${INSTALL_DIR}"; then + die "Installation failed. Check version or network connection." +fi + +log "โœ“ Successfully installed ${TOOL_NAME} to ${INSTALL_DIR}/${TOOL_NAME}" + +# Run tool version to verify +"${INSTALL_DIR}/${TOOL_NAME}" version || die "Installed binary failed to run (${INSTALL_DIR}/${TOOL_NAME})" diff --git a/.taskfiles/scripts/install_bicep.sh b/.taskfiles/scripts/install_bicep.sh new file mode 100755 index 0000000..6ef084b --- /dev/null +++ b/.taskfiles/scripts/install_bicep.sh @@ -0,0 +1,137 @@ +#!/bin/bash + +set -euo pipefail + +# Constants +readonly GITHUB_OWNER="azure" +readonly GITHUB_REPO="bicep" +readonly TOOL_NAME="bicep" + +# Configuration (can be overridden by env) +VERSION="${1:-${VERSION:-latest}}" +INSTALL_DIR="${2:-${INSTALL_DIR:-}}" + +tempDir="" + +# Logging helper +log() { + echo "-> $*" >&2 +} + +# Error handling helper +die() { + echo "X Error: $*" >&2 + exit "${2:-1}" +} +# Help message +usage() { + cat < "latest", numeric -> add "v" prefix +if [[ -z "${VERSION//[[:space:]]/}" ]]; then + VERSION="latest" +elif [[ "${VERSION}" != "latest" && "${VERSION}" =~ ^[0-9] ]]; then + VERSION="v${VERSION}" +fi + +# Determine install directory +if [[ -z "${INSTALL_DIR}" ]]; then + if [[ "${EUID}" -eq 0 ]]; then + INSTALL_DIR="/usr/local/bin" + else + INSTALL_DIR="${HOME}/.local/bin" + fi +fi + +# Check dependencies +for dep in curl jq; do + command -v "${dep}" >/dev/null 2>&1 || die "Missing required dependency: ${dep}" +done + +# Create install directory if it doesn't exist +if [[ ! -d "${INSTALL_DIR}" ]]; then + mkdir -p "${INSTALL_DIR}" || die "Cannot create install directory ${INSTALL_DIR}" +fi + +# Detect architecture +archRaw="$(uname -m)" +case "${archRaw}" in + x86_64 | amd64) arch="x64" ;; + arm64 | aarch64) arch="arm64" ;; + *) die "Unsupported architecture: ${archRaw}" ;; +esac + +log "Installing ${TOOL_NAME} (${VERSION}) for ${arch} to ${INSTALL_DIR}" + +# GitHub API authentication +ghAuthHeader=(-H "Accept: application/vnd.github+json") +if [[ -n "${GITHUB_TOKEN:-}" ]]; then + ghAuthHeader+=(-H "Authorization: Bearer ${GITHUB_TOKEN}") +fi + +# Create temp directory +tempDir="$(mktemp -d)" || die "Failed to create temp directory" + +# Fetch release info and download URL +log "Fetching release information" +if [[ "${VERSION}" == "latest" ]]; then + apiUrl="https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases/latest" +else + apiUrl="https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases/tags/${VERSION}" +fi + +releaseJson="${tempDir}/release.json" +if ! curl "${ghAuthHeader[@]}" -fsSL "${apiUrl}" -o "${releaseJson}"; then + die "Failed to fetch release information. Check version or network connection." +fi + +# Extract download URL +downloadUrl="$(jq -r --arg arch "${arch}" \ + '.assets[] | select(.browser_download_url | endswith("-linux-\($arch)")) | .browser_download_url' \ + "${releaseJson}")" + +[[ -n "${downloadUrl}" ]] || die "No asset found for ${arch}" + +log "Downloading ${downloadUrl}" +binaryPath="${tempDir}/${TOOL_NAME}" +curl "${ghAuthHeader[@]}" -fsSL "${downloadUrl}" -o "${binaryPath}" || die "Download failed" + +# Install binary +log "Installing binary" +install -Dm0755 "${binaryPath}" "${INSTALL_DIR}/${TOOL_NAME}" || die "Failed to install binary" + +log "โœ“ Successfully installed ${TOOL_NAME} to ${INSTALL_DIR}/${TOOL_NAME}" + +# Run tool version to verify +"${INSTALL_DIR}/${TOOL_NAME}" --version || die "Installed binary failed to run (${INSTALL_DIR}/${TOOL_NAME})" diff --git a/.taskfiles/scripts/install_ghalint.ps1 b/.taskfiles/scripts/install_ghalint.ps1 new file mode 100755 index 0000000..dfd727b --- /dev/null +++ b/.taskfiles/scripts/install_ghalint.ps1 @@ -0,0 +1,148 @@ +[CmdletBinding()] +param( + [Parameter(Position = 0)] + [string]$Version = $env:VERSION, + + [Parameter(Position = 1)] + [string]$InstallDir = $env:INSTALL_DIR +) + +$ErrorActionPreference = 'Stop' + +# Constants +$GitHubOwner = 'suzuki-shunsuke' +$GitHubRepo = 'ghalint' +$ToolName = 'ghalint' + +function Write-Log { + param([Parameter(Mandatory)] [string]$Message) + Write-Host "-> $Message" +} + +function Show-Usage { + @" +Usage: install_ghalint.ps1 [VERSION] [INSTALL_DIR] + +Positional arguments: + VERSION Version to install (default: latest) + INSTALL_DIR Custom install directory + +Environment variables: + VERSION Desired version (default: latest) + INSTALL_DIR Install directory override + GITHUB_TOKEN GitHub token for API authentication + +Examples: + ./install_ghalint.ps1 # Install latest + ./install_ghalint.ps1 1.5.4 # Install 1.5.4 + ./install_ghalint.ps1 1.5.4 "$HOME\.local\bin" # Install 1.5.4 to a custom dir +"@ +} + +# Help +if ($args -contains '-h' -or $args -contains '--help') { + Show-Usage + exit 0 +} + +# Normalize version +if ([string]::IsNullOrWhiteSpace($Version)) { + $Version = 'latest' +} + +$versionTag = if ($Version -eq 'latest') { + 'latest' +} +elseif ($Version -match '^v') { + $Version +} +else { + "v$Version" +} + +# Determine install directory (simple, user-first default) +if ([string]::IsNullOrWhiteSpace($InstallDir)) { + $InstallDir = Join-Path $HOME '.local\bin' +} + +if (-not (Test-Path -LiteralPath $InstallDir)) { + New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null +} + +# Detect architecture +$arch = switch ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture) { + 'X64' { 'amd64' } + 'Arm64' { 'arm64' } + default { throw "Unsupported architecture: $([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)" } +} + +Write-Log "Installing $ToolName ($versionTag) to $InstallDir" + +# GitHub API +$headers = @{ 'Accept' = 'application/vnd.github+json' } +if (-not [string]::IsNullOrWhiteSpace($env:GITHUB_TOKEN)) { + $headers['Authorization'] = "Bearer $($env:GITHUB_TOKEN)" +} + +$apiUrl = if ($versionTag -eq 'latest') { + "https://api.github.com/repos/$GitHubOwner/$GitHubRepo/releases/latest" +} +else { + "https://api.github.com/repos/$GitHubOwner/$GitHubRepo/releases/tags/$versionTag" +} + +$tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString('n')) +New-Item -ItemType Directory -Path $tempDir -Force | Out-Null + +try { + Write-Log 'Fetching release information' + $release = Invoke-RestMethod -Headers $headers -Uri $apiUrl + + # Asset pattern (example): ghalint_1.5.4_windows_amd64.zip + $verNoV = if ($versionTag -eq 'latest') { + # use tag_name to compute asset name + ($release.tag_name -replace '^v', '') + } + else { + ($versionTag -replace '^v', '') + } + + $assetName = "${ToolName}_${verNoV}_windows_${arch}.zip" + $asset = $release.assets | Where-Object { $_.name -eq $assetName } | Select-Object -First 1 + if (-not $asset) { + throw "No asset found for architecture $arch (expected $assetName)" + } + + $zipPath = Join-Path $tempDir $assetName + Write-Log "Downloading $($asset.browser_download_url)" + Invoke-WebRequest -Headers $headers -Uri $asset.browser_download_url -OutFile $zipPath + + Write-Log 'Extracting archive' + Expand-Archive -Path $zipPath -DestinationPath $tempDir -Force + + $exeSource = Join-Path $tempDir "$ToolName.exe" + if (-not (Test-Path -LiteralPath $exeSource)) { + $found = Get-ChildItem -Path $tempDir -Recurse -Filter "$ToolName.exe" -File -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($found) { + $exeSource = $found.FullName + } + } + + if (-not (Test-Path -LiteralPath $exeSource)) { + throw "Binary not found in archive ($ToolName.exe)" + } + + $exeDest = Join-Path $InstallDir "$ToolName.exe" + + Write-Log 'Installing binary' + Copy-Item -LiteralPath $exeSource -Destination $exeDest -Force + + Write-Log "โœ“ Successfully installed $ToolName to $exeDest" + + & $exeDest --version | Out-Null +} +finally { + if (Test-Path -LiteralPath $tempDir) { + Remove-Item -LiteralPath $tempDir -Recurse -Force -ErrorAction SilentlyContinue + } +} diff --git a/.taskfiles/scripts/install_ghalint.sh b/.taskfiles/scripts/install_ghalint.sh new file mode 100755 index 0000000..ac60d38 --- /dev/null +++ b/.taskfiles/scripts/install_ghalint.sh @@ -0,0 +1,145 @@ +#!/bin/bash + +set -euo pipefail + +# Constants +readonly GITHUB_OWNER="suzuki-shunsuke" +readonly GITHUB_REPO="ghalint" +readonly TOOL_NAME="ghalint" + +# Configuration (can be overridden by env) +VERSION="${1:-${VERSION:-latest}}" +INSTALL_DIR="${2:-${INSTALL_DIR:-}}" + +tempDir="" + +# Logging helper +log() { + echo "-> $*" >&2 +} + +# Error handling helper +die() { + echo "X Error: $*" >&2 + exit "${2:-1}" +} +# Help message +usage() { + cat < "latest", numeric -> add "v" prefix +if [[ -z "${VERSION//[[:space:]]/}" ]]; then + VERSION="latest" +elif [[ "${VERSION}" != "latest" && "${VERSION}" =~ ^[0-9] ]]; then + VERSION="v${VERSION}" +fi + +# Determine install directory +if [[ -z "${INSTALL_DIR}" ]]; then + if [[ "${EUID}" -eq 0 ]]; then + INSTALL_DIR="/usr/local/bin" + else + INSTALL_DIR="${HOME}/.local/bin" + fi +fi + +# Check dependencies +for dep in curl jq tar; do + command -v "${dep}" >/dev/null 2>&1 || die "Missing required dependency: ${dep}" +done + +# Create install directory if it doesn't exist +if [[ ! -d "${INSTALL_DIR}" ]]; then + mkdir -p "${INSTALL_DIR}" || die "Cannot create install directory ${INSTALL_DIR}" +fi + +# Detect architecture +archRaw="$(uname -m)" +case "${archRaw}" in + x86_64 | amd64) arch="amd64" ;; + arm64 | aarch64) arch="arm64" ;; + # armv7l | armv6l) arch="arm" ;; + # i386 | i686) arch="386" ;; + *) die "Unsupported architecture: ${archRaw}" ;; +esac + +log "Installing ${TOOL_NAME} (${VERSION}) to ${INSTALL_DIR}" + +# GitHub API authentication +ghAuthHeader=(-H "Accept: application/vnd.github+json") +if [[ -n "${GITHUB_TOKEN:-}" ]]; then + ghAuthHeader+=(-H "Authorization: Bearer ${GITHUB_TOKEN}") +fi + +# Create temp directory +tempDir="$(mktemp -d)" || die "Failed to create temp directory" + +# Fetch release info and download URL +log "Fetching release information" +if [[ "${VERSION}" == "latest" ]]; then + apiUrl="https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases/latest" +else + apiUrl="https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases/tags/${VERSION}" +fi + +releaseJson="${tempDir}/release.json" +if ! curl "${ghAuthHeader[@]}" -fsSL "${apiUrl}" -o "${releaseJson}"; then + die "Failed to fetch release information. Check version or network connection." +fi + +# Extract download URL +downloadUrl="$(jq -r --arg arch "${arch}" \ + '.assets[] | select(.browser_download_url | endswith("_linux_\($arch).tar.gz")) | .browser_download_url' \ + "${releaseJson}")" + +[[ -n "${downloadUrl}" ]] || die "No asset found for architecture ${arch}" + +log "Downloading ${downloadUrl}" +archivePath="${tempDir}/${TOOL_NAME}.tar.gz" +curl "${ghAuthHeader[@]}" -fsSL "${downloadUrl}" -o "${archivePath}" || die "Download failed" + +# Extract binary +log "Extracting archive" +tar -xf "${archivePath}" -C "${tempDir}" "${TOOL_NAME}" || die "Extraction failed" +[[ -f "${tempDir}/${TOOL_NAME}" ]] || die "Binary not found in archive" + +# Install binary +log "Installing binary" +mv "${tempDir}/${TOOL_NAME}" "${INSTALL_DIR}/${TOOL_NAME}" || die "Failed to install binary" +chmod 0755 "${INSTALL_DIR}/${TOOL_NAME}" || die "Failed to set permissions" + +log "โœ“ Successfully installed ${TOOL_NAME} to ${INSTALL_DIR}/${TOOL_NAME}" + +# Run tool version to verify +"${INSTALL_DIR}/${TOOL_NAME}" --version || die "Installed binary failed to run (${INSTALL_DIR}/${TOOL_NAME})" diff --git a/.taskfiles/scripts/install_golangci-lint.sh b/.taskfiles/scripts/install_golangci-lint.sh new file mode 100755 index 0000000..6f658a5 --- /dev/null +++ b/.taskfiles/scripts/install_golangci-lint.sh @@ -0,0 +1,96 @@ +#!/bin/bash + +set -euo pipefail + +# Constants +readonly GITHUB_OWNER="golangci" +readonly GITHUB_REPO="golangci-lint" +readonly TOOL_NAME="golangci-lint" +readonly INSTALL_SCRIPT_URL="https://raw.githubusercontent.com/${GITHUB_OWNER}/${GITHUB_REPO}/master/install.sh" + +# Configuration (can be overridden by env) +VERSION="${1:-${VERSION:-latest}}" +INSTALL_DIR="${2:-${INSTALL_DIR:-}}" + +# Logging helper +log() { + echo "-> $*" >&2 +} + +# Error handling helper +die() { + echo "X Error: $*" >&2 + exit "${2:-1}" +} + +# Help message +usage() { + cat < "latest", numeric -> add "v" prefix +if [[ -z "${VERSION//[[:space:]]/}" ]]; then + VERSION="latest" +elif [[ "${VERSION}" != "latest" && "${VERSION}" =~ ^[0-9] ]]; then + VERSION="v${VERSION}" +fi + +# Determine install directory +if [[ -z "${INSTALL_DIR}" ]]; then + if [[ "${EUID}" -eq 0 ]]; then + INSTALL_DIR="/usr/local/bin" + else + INSTALL_DIR="${HOME}/.local/bin" + fi +fi + +# Check dependencies +command -v curl >/dev/null 2>&1 || die "Missing required dependency: curl" + +# Create install directory if it doesn't exist +if [[ ! -d "${INSTALL_DIR}" ]]; then + mkdir -p "${INSTALL_DIR}" || die "Cannot create install directory ${INSTALL_DIR}" +fi + +log "Installing ${TOOL_NAME} (${VERSION}) to ${INSTALL_DIR}" + +# GitHub API authentication +if [[ -n "${GITHUB_TOKEN:-}" ]]; then + ghAuthHeader=(-H "Authorization: Bearer ${GITHUB_TOKEN}") +else + ghAuthHeader=() +fi + +# Execute remote installation script +log "Fetching and executing official installation script" +if ! curl "${ghAuthHeader[@]}" -fsSL "${INSTALL_SCRIPT_URL}" | sh -s -- -b "${INSTALL_DIR}" "${VERSION}"; then + die "Installation failed. Check version or network connection." +fi + +log "โœ“ Successfully installed ${TOOL_NAME} to ${INSTALL_DIR}/${TOOL_NAME}" + +# Run tool version to verify +"${INSTALL_DIR}/${TOOL_NAME}" --version || die "Installed binary failed to run (${INSTALL_DIR}/${TOOL_NAME})" diff --git a/.taskfiles/scripts/install_goreleaser.sh b/.taskfiles/scripts/install_goreleaser.sh new file mode 100755 index 0000000..a84efcc --- /dev/null +++ b/.taskfiles/scripts/install_goreleaser.sh @@ -0,0 +1,161 @@ +#!/bin/bash + +set -euo pipefail + +# Constants +readonly GITHUB_OWNER="goreleaser" +readonly GITHUB_REPO="goreleaser" +readonly TOOL_NAME="goreleaser" + +# Configuration (can be overridden by env) +VERSION="${1:-${VERSION:-latest}}" +INSTALL_DIR="${2:-${INSTALL_DIR:-}}" + +tempDir="" + +# Logging helper +log() { + echo "-> $*" >&2 +} + +# Error handling helper +die() { + echo "X Error: $*" >&2 + exit "${2:-1}" +} + +# Help message +usage() { + cat < "latest", numeric -> add "v" prefix +if [[ -z "${VERSION//[[:space:]]/}" ]]; then + VERSION="latest" +elif [[ "${VERSION}" != "latest" && "${VERSION}" =~ ^[0-9] ]]; then + VERSION="v${VERSION}" +fi + +# Determine install directory +if [[ -z "${INSTALL_DIR}" ]]; then + if [[ "${EUID}" -eq 0 ]]; then + INSTALL_DIR="/usr/local/bin" + else + INSTALL_DIR="${HOME}/.local/bin" + fi +fi + +# Check dependencies +for dep in curl jq tar; do + command -v "${dep}" >/dev/null 2>&1 || die "Missing required dependency: ${dep}" +done + +# Create install directory if it doesn't exist +if [[ ! -d "${INSTALL_DIR}" ]]; then + mkdir -p "${INSTALL_DIR}" || die "Cannot create install directory ${INSTALL_DIR}" +fi + +# Detect OS +osRaw="$(uname -s)" +case "${osRaw}" in + Linux) os="Linux" ;; + Darwin) os="Darwin" ;; + *) die "Unsupported operating system: ${osRaw}" ;; +esac + +# Detect architecture +archRaw="$(uname -m)" +case "${archRaw}" in + x86_64 | amd64) arch="x86_64" ;; + arm64 | aarch64) arch="arm64" ;; + armv7l | armv6l) arch="armv7" ;; + i386 | i686) arch="i386" ;; + *) die "Unsupported architecture: ${archRaw}" ;; +esac + +log "Installing ${TOOL_NAME} (${VERSION}) for ${os}/${arch} to ${INSTALL_DIR}" + +# GitHub API authentication +ghAuthHeader=(-H "Accept: application/vnd.github+json") +ghAuthHeader+=(-H "User-Agent: ${TOOL_NAME}-installer") +if [[ -n "${GITHUB_TOKEN:-}" ]]; then + ghAuthHeader+=(-H "Authorization: Bearer ${GITHUB_TOKEN}") +fi + +# Create temp directory +tempDir="$(mktemp -d)" || die "Failed to create temp directory" + +# Fetch release info and download URL +log "Fetching release information" +if [[ "${VERSION}" == "latest" ]]; then + apiUrl="https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases/latest" +else + apiUrl="https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases/tags/${VERSION}" +fi + +releaseJson="${tempDir}/release.json" +if ! curl "${ghAuthHeader[@]}" -fsSL "${apiUrl}" -o "${releaseJson}"; then + die "Failed to fetch release information. Check version or network connection." +fi + +# Extract download URL (pattern: goreleaser__.tar.gz) +downloadUrl="$(jq -r --arg os "${os}" --arg arch "${arch}" \ + '.assets[] | select(.browser_download_url | test("goreleaser_\($os)_\($arch)\\.tar\\.gz$")) | .browser_download_url' \ + "${releaseJson}")" + +[[ -n "${downloadUrl}" ]] || die "No asset found for ${os}/${arch}" + +log "Downloading ${downloadUrl}" +archivePath="${tempDir}/${TOOL_NAME}.tar.gz" +curl "${ghAuthHeader[@]}" -fsSL "${downloadUrl}" -o "${archivePath}" || die "Download failed" + +# Extract binary +log "Extracting archive" +tar -xf "${archivePath}" -C "${tempDir}" "${TOOL_NAME}" || die "Extraction failed" +[[ -f "${tempDir}/${TOOL_NAME}" ]] || die "Binary not found in archive" + +# Install binary +log "Installing binary" +if [[ "${os}" == "Darwin" ]]; then + # macOS install command may not support -D flag in all versions + mkdir -p "${INSTALL_DIR}" || die "Failed to create install directory" + install -m 0755 "${tempDir}/${TOOL_NAME}" "${INSTALL_DIR}/${TOOL_NAME}" || die "Failed to install binary" +else + # Linux install with -D flag + install -Dm0755 "${tempDir}/${TOOL_NAME}" "${INSTALL_DIR}/${TOOL_NAME}" || die "Failed to install binary" +fi + +log "โœ“ Successfully installed ${TOOL_NAME} to ${INSTALL_DIR}/${TOOL_NAME}" + +# Run tool version to verify +"${INSTALL_DIR}/${TOOL_NAME}" --version || die "Installed binary failed to run (${INSTALL_DIR}/${TOOL_NAME})" diff --git a/.taskfiles/scripts/install_opentofu.sh b/.taskfiles/scripts/install_opentofu.sh new file mode 100755 index 0000000..9f835e2 --- /dev/null +++ b/.taskfiles/scripts/install_opentofu.sh @@ -0,0 +1,87 @@ +#!/bin/bash + +set -euo pipefail + +# Constants +readonly TOOL_NAME="tofu" +readonly INSTALL_SCRIPT_URL="https://get.opentofu.org/install-opentofu.sh" + +# Configuration (can be overridden by env) +VERSION="${1:-${VERSION:-latest}}" +INSTALL_DIR="${2:-${INSTALL_DIR:-}}" + +# Logging helper +log() { + echo "-> $*" >&2 +} + +# Error handling helper +die() { + echo "X Error: $*" >&2 + exit "${2:-1}" +} + +# Help message +usage() { + cat < "latest", numeric -> add "v" prefix +if [[ -z "${VERSION//[[:space:]]/}" ]]; then + VERSION="latest" +elif [[ "${VERSION}" != "latest" && "${VERSION}" =~ ^[0-9] ]]; then + VERSION="v${VERSION}" +fi + +# Determine install directory +if [[ -z "${INSTALL_DIR}" ]]; then + if [[ "${EUID}" -eq 0 ]]; then + INSTALL_DIR="/usr/local/bin" + else + INSTALL_DIR="${HOME}/.local/bin" + fi +fi + +# Check dependencies +command -v curl >/dev/null 2>&1 || die "Missing required dependency: curl" + +# Create install directory if it doesn't exist +if [[ ! -d "${INSTALL_DIR}" ]]; then + mkdir -p "${INSTALL_DIR}" || die "Cannot create install directory ${INSTALL_DIR}" +fi + +log "Installing ${TOOL_NAME} (${VERSION}) to ${INSTALL_DIR}" + +# Execute remote installation script +log "Fetching and executing official installation script" +if ! curl -fsSL "${INSTALL_SCRIPT_URL}" | sh -s -- --install-method standalone --opentofu-version "${VERSION}" --install-path "${INSTALL_DIR}"; then + die "Installation failed. Check version or network connection." +fi + +log "โœ“ Successfully installed ${TOOL_NAME} to ${INSTALL_DIR}/${TOOL_NAME}" + +# Run tool version to verify +"${INSTALL_DIR}/${TOOL_NAME}" -version || die "Installed binary failed to run (${INSTALL_DIR}/${TOOL_NAME})" diff --git a/.taskfiles/scripts/install_pinact.ps1 b/.taskfiles/scripts/install_pinact.ps1 new file mode 100755 index 0000000..00e1105 --- /dev/null +++ b/.taskfiles/scripts/install_pinact.ps1 @@ -0,0 +1,144 @@ +[CmdletBinding()] +param( + [Parameter(Position = 0)] + [string]$Version = $env:VERSION, + + [Parameter(Position = 1)] + [string]$InstallDir = $env:INSTALL_DIR +) + +$ErrorActionPreference = 'Stop' + +# Constants +$GitHubOwner = 'suzuki-shunsuke' +$GitHubRepo = 'pinact' +$ToolName = 'pinact' + +function Write-Log { + param([Parameter(Mandatory)] [string]$Message) + Write-Host "-> $Message" +} + +function Die { + param( + [Parameter(Mandatory)] [string]$Message, + [int]$ExitCode = 1 + ) + Write-Error "X Error: $Message" + exit $ExitCode +} + +function Show-Usage { + @" +Usage: install_pinact.ps1 [VERSION] [INSTALL_DIR] + +Positional arguments: + VERSION Version to install (default: latest) + INSTALL_DIR Custom install directory + +Environment variables: + VERSION Desired version (default: latest) + INSTALL_DIR Install directory override + GITHUB_TOKEN GitHub token for API authentication + +Examples: + ./install_pinact.ps1 # Install latest + ./install_pinact.ps1 3.6.0 # Install 3.6.0 + ./install_pinact.ps1 3.6.0 "$HOME\.local\bin" # Install 3.6.0 to a custom dir +"@ +} + +# Help +if ($args -contains '-h' -or $args -contains '--help' -or $PSBoundParameters.ContainsKey('Help')) { + Show-Usage + exit 0 +} + +# Normalize version +if ([string]::IsNullOrWhiteSpace($Version)) { + $Version = 'latest' +} +elseif ($Version -match '^[0-9]') { + $Version = "v$Version" +} + +# Determine install directory (simple, user-first default) +if ([string]::IsNullOrWhiteSpace($InstallDir)) { + $InstallDir = Join-Path $HOME '.local\bin' +} + +# Ensure install directory exists +if (-not (Test-Path -LiteralPath $InstallDir)) { + New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null +} + +# Detect architecture +$arch = switch ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture) { + 'X64' { 'amd64' } + 'Arm64' { 'arm64' } + default { Die "Unsupported architecture: $([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)" } +} + +Write-Log "Installing $ToolName ($Version) to $InstallDir" + +# GitHub API +$headers = @{ 'Accept' = 'application/vnd.github+json' } +if (-not [string]::IsNullOrWhiteSpace($env:GITHUB_TOKEN)) { + $headers['Authorization'] = "Bearer $($env:GITHUB_TOKEN)" +} + +$apiUrl = if ($Version -eq 'latest') { + "https://api.github.com/repos/$GitHubOwner/$GitHubRepo/releases/latest" +} +else { + "https://api.github.com/repos/$GitHubOwner/$GitHubRepo/releases/tags/$Version" +} + +$tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString('n')) +New-Item -ItemType Directory -Path $tempDir -Force | Out-Null + +try { + Write-Log 'Fetching release information' + $release = Invoke-RestMethod -Headers $headers -Uri $apiUrl + + $assetName = "${ToolName}_windows_${arch}.zip" + $asset = $release.assets | Where-Object { $_.name -eq $assetName } | Select-Object -First 1 + if (-not $asset) { + Die "No asset found for architecture $arch (expected $assetName)" + } + + $zipPath = Join-Path $tempDir $assetName + Write-Log "Downloading $($asset.browser_download_url)" + Invoke-WebRequest -Headers $headers -Uri $asset.browser_download_url -OutFile $zipPath + + Write-Log 'Extracting archive' + Expand-Archive -Path $zipPath -DestinationPath $tempDir -Force + + $exeSource = Join-Path $tempDir "$ToolName.exe" + if (-not (Test-Path -LiteralPath $exeSource)) { + # Fallback: search anywhere in extracted tree + $found = Get-ChildItem -Path $tempDir -Recurse -Filter "$ToolName.exe" -File -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($found) { + $exeSource = $found.FullName + } + } + + if (-not (Test-Path -LiteralPath $exeSource)) { + Die "Binary not found in archive ($ToolName.exe)" + } + + $exeDest = Join-Path $InstallDir "$ToolName.exe" + + Write-Log 'Installing binary' + Copy-Item -LiteralPath $exeSource -Destination $exeDest -Force + + Write-Log "โœ“ Successfully installed $ToolName to $exeDest" + + # Verify + & $exeDest version | Out-Null +} +finally { + if (Test-Path -LiteralPath $tempDir) { + Remove-Item -LiteralPath $tempDir -Recurse -Force -ErrorAction SilentlyContinue + } +} diff --git a/.taskfiles/scripts/install_pinact.sh b/.taskfiles/scripts/install_pinact.sh new file mode 100755 index 0000000..c9b8656 --- /dev/null +++ b/.taskfiles/scripts/install_pinact.sh @@ -0,0 +1,141 @@ +#!/bin/bash + +set -euo pipefail + +# Constants +readonly GITHUB_OWNER="suzuki-shunsuke" +readonly GITHUB_REPO="pinact" +readonly TOOL_NAME="pinact" + +# Configuration (can be overridden by env) +VERSION="${1:-${VERSION:-latest}}" +INSTALL_DIR="${2:-${INSTALL_DIR:-}}" + +tempDir="" + +log() { + echo "-> $*" >&2 +} + +die() { + echo "X Error: $*" >&2 + exit "${2:-1}" +} +usage() { + cat < "latest", numeric -> add "v" prefix +if [[ -z "${VERSION//[[:space:]]/}" ]]; then + VERSION="latest" +elif [[ "${VERSION}" != "latest" && "${VERSION}" =~ ^[0-9] ]]; then + VERSION="v${VERSION}" +fi + +# Determine install directory +if [[ -z "${INSTALL_DIR}" ]]; then + if [[ "${EUID}" -eq 0 ]]; then + INSTALL_DIR="/usr/local/bin" + else + INSTALL_DIR="${HOME}/.local/bin" + fi +fi + +# Check dependencies +for dep in curl jq tar; do + command -v "${dep}" >/dev/null 2>&1 || die "Missing required dependency: ${dep}" +done + +# Create install directory if it doesn't exist +if [[ ! -d "${INSTALL_DIR}" ]]; then + mkdir -p "${INSTALL_DIR}" || die "Cannot create install directory ${INSTALL_DIR}" +fi + +# Detect architecture +archRaw="$(uname -m)" +case "${archRaw}" in + x86_64 | amd64) arch="amd64" ;; + arm64 | aarch64) arch="arm64" ;; + # armv7l | armv6l) arch="arm" ;; + # i386 | i686) arch="386" ;; + *) die "Unsupported architecture: ${archRaw}" ;; +esac + +log "Installing ${TOOL_NAME} (${VERSION}) to ${INSTALL_DIR}" + +# GitHub API authentication +ghAuthHeader=(-H "Accept: application/vnd.github+json") +if [[ -n "${GITHUB_TOKEN:-}" ]]; then + ghAuthHeader+=(-H "Authorization: Bearer ${GITHUB_TOKEN}") +fi + +# Create temp directory +tempDir="$(mktemp -d)" || die "Failed to create temp directory" + +# Fetch release info and download URL +log "Fetching release information" +if [[ "${VERSION}" == "latest" ]]; then + apiUrl="https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases/latest" +else + apiUrl="https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases/tags/${VERSION}" +fi + +releaseJson="${tempDir}/release.json" +if ! curl "${ghAuthHeader[@]}" -fsSL "${apiUrl}" -o "${releaseJson}"; then + die "Failed to fetch release information. Check version or network connection." +fi + +# Extract download URL +downloadUrl="$(jq -r --arg arch "${arch}" \ + '.assets[] | select(.browser_download_url | endswith("_linux_\($arch).tar.gz")) | .browser_download_url' \ + "${releaseJson}")" + +[[ -n "${downloadUrl}" ]] || die "No asset found for architecture ${arch}" + +log "Downloading ${downloadUrl}" +archivePath="${tempDir}/${TOOL_NAME}.tar.gz" +curl "${ghAuthHeader[@]}" -fsSL "${downloadUrl}" -o "${archivePath}" || die "Download failed" + +# Extract binary +log "Extracting archive" +tar -xf "${archivePath}" -C "${tempDir}" "${TOOL_NAME}" || die "Extraction failed" +[[ -f "${tempDir}/${TOOL_NAME}" ]] || die "Binary not found in archive" + +# Install binary +log "Installing binary" +mv "${tempDir}/${TOOL_NAME}" "${INSTALL_DIR}/${TOOL_NAME}" || die "Failed to install binary" +chmod 0755 "${INSTALL_DIR}/${TOOL_NAME}" || die "Failed to set permissions" + +log "โœ“ Successfully installed ${TOOL_NAME} to ${INSTALL_DIR}/${TOOL_NAME}" + +# Run tool version to verify +"${INSTALL_DIR}/${TOOL_NAME}" --version || die "Installed binary failed to run (${INSTALL_DIR}/${TOOL_NAME})" diff --git a/.taskfiles/scripts/install_shellcheck.sh b/.taskfiles/scripts/install_shellcheck.sh new file mode 100755 index 0000000..77fc0f1 --- /dev/null +++ b/.taskfiles/scripts/install_shellcheck.sh @@ -0,0 +1,143 @@ +#!/bin/bash + +set -euo pipefail + +# Constants +readonly GITHUB_OWNER="koalaman" +readonly GITHUB_REPO="shellcheck" +readonly TOOL_NAME="shellcheck" + +# Configuration (can be overridden by env) +VERSION="${1:-${VERSION:-latest}}" +INSTALL_DIR="${2:-${INSTALL_DIR:-}}" + +tempDir="" + +log() { + echo "-> $*" >&2 +} + +die() { + echo "X Error: $*" >&2 + exit "${2:-1}" +} +usage() { + cat < "latest", numeric -> add "v" prefix +if [[ -z "${VERSION//[[:space:]]/}" ]]; then + VERSION="latest" +elif [[ "${VERSION}" != "latest" && "${VERSION}" =~ ^[0-9] ]]; then + VERSION="v${VERSION}" +fi + +# Determine install directory +if [[ -z "${INSTALL_DIR}" ]]; then + if [[ "${EUID}" -eq 0 ]]; then + INSTALL_DIR="/usr/local/bin" + else + INSTALL_DIR="${HOME}/.local/bin" + fi +fi + +# Check dependencies +for dep in curl jq tar xz; do + command -v "${dep}" >/dev/null 2>&1 || die "Missing required dependency: ${dep}" +done + +# Create install directory if it doesn't exist +if [[ ! -d "${INSTALL_DIR}" ]]; then + mkdir -p "${INSTALL_DIR}" || die "Cannot create install directory ${INSTALL_DIR}" +fi + +# Detect architecture +archRaw="$(uname -m)" +case "${archRaw}" in + x86_64 | amd64) arch="x86_64" ;; + arm64 | aarch64) arch="aarch64" ;; + *) die "Unsupported architecture: ${archRaw}" ;; +esac + +log "Installing ${TOOL_NAME} (${VERSION}) to ${INSTALL_DIR}" + +# GitHub API authentication +ghAuthHeader=(-H "Accept: application/vnd.github+json") +if [[ -n "${GITHUB_TOKEN:-}" ]]; then + ghAuthHeader+=(-H "Authorization: Bearer ${GITHUB_TOKEN}") +fi + +# Create temp directory +tempDir="$(mktemp -d)" || die "Failed to create temp directory" + +# Fetch release info and download URL +log "Fetching release information" +if [[ "${VERSION}" == "latest" ]]; then + apiUrl="https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases/latest" +else + apiUrl="https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases/tags/${VERSION}" +fi + +releaseJson="${tempDir}/release.json" +if ! curl "${ghAuthHeader[@]}" -fsSL "${apiUrl}" -o "${releaseJson}"; then + die "Failed to fetch release information. Check version or network connection." +fi + +# Extract download URL +downloadUrl="$(jq -r --arg arch "${arch}" \ + '.assets[] | select(.browser_download_url | endswith("linux.\($arch).tar.xz")) | .browser_download_url' \ + "${releaseJson}")" + +[[ -n "${downloadUrl}" ]] || die "No asset found for architecture ${arch}" + +log "Downloading ${downloadUrl}" +archivePath="${tempDir}/${TOOL_NAME}.tar.xz" +curl "${ghAuthHeader[@]}" -fsSL "${downloadUrl}" -o "${archivePath}" || die "Download failed" + +# Extract binary +log "Extracting archive" +tar -xf "${archivePath}" -C "${tempDir}" || die "Extraction failed" +# Find and move the binary (it's in a versioned subdirectory) +binaryPath="$(find "${tempDir}" -name "${TOOL_NAME}" -type f)" || die "Binary not found in archive" +[[ -n "${binaryPath}" ]] || die "Binary not found in archive" +mv "${binaryPath}" "${tempDir}/${TOOL_NAME}" || die "Failed to move binary" +[[ -f "${tempDir}/${TOOL_NAME}" ]] || die "Binary not found in archive" + +# Install binary +log "Installing binary" +mv "${tempDir}/${TOOL_NAME}" "${INSTALL_DIR}/${TOOL_NAME}" || die "Failed to install binary" +chmod 0755 "${INSTALL_DIR}/${TOOL_NAME}" || die "Failed to set permissions" + +log "โœ“ Successfully installed ${TOOL_NAME} to ${INSTALL_DIR}/${TOOL_NAME}" + +# Run tool version to verify +"${INSTALL_DIR}/${TOOL_NAME}" --version || die "Installed binary failed to run (${INSTALL_DIR}/${TOOL_NAME})" diff --git a/.taskfiles/scripts/install_shfmt.sh b/.taskfiles/scripts/install_shfmt.sh new file mode 100755 index 0000000..2d57f07 --- /dev/null +++ b/.taskfiles/scripts/install_shfmt.sh @@ -0,0 +1,150 @@ +#!/bin/bash + +set -euo pipefail + +# Constants +readonly GITHUB_OWNER="mvdan" +readonly GITHUB_REPO="sh" +readonly TOOL_NAME="shfmt" + +# Configuration (can be overridden by env) +VERSION="${1:-${VERSION:-latest}}" +INSTALL_DIR="${2:-${INSTALL_DIR:-}}" + +tempDir="" + +log() { + echo "-> $*" >&2 +} + +die() { + echo "X Error: $*" >&2 + exit "${2:-1}" +} +usage() { + cat < "latest", numeric -> add "v" prefix +if [[ -z "${VERSION//[[:space:]]/}" ]]; then + VERSION="latest" +elif [[ "${VERSION}" != "latest" && "${VERSION}" =~ ^[0-9] ]]; then + VERSION="v${VERSION}" +fi + +# Determine install directory +if [[ -z "${INSTALL_DIR}" ]]; then + if [[ "${EUID}" -eq 0 ]]; then + INSTALL_DIR="/usr/local/bin" + else + INSTALL_DIR="${HOME}/.local/bin" + fi +fi + +# Check dependencies +for dep in curl jq tar; do + command -v "${dep}" >/dev/null 2>&1 || die "Missing required dependency: ${dep}" +done + +# Create install directory if it doesn't exist +if [[ ! -d "${INSTALL_DIR}" ]]; then + mkdir -p "${INSTALL_DIR}" || die "Cannot create install directory ${INSTALL_DIR}" +fi + +# Detect OS +osRaw="$(uname -s)" +case "${osRaw}" in + Linux) os="linux" ;; + Darwin) os="darwin" ;; + *) die "Unsupported operating system: ${osRaw}" ;; +esac + +# Detect architecture +archRaw="$(uname -m)" +case "${archRaw}" in + x86_64 | amd64) arch="amd64" ;; + arm64 | aarch64) arch="arm64" ;; + armv7l | armv6l) arch="arm" ;; + i386 | i686) arch="386" ;; + *) die "Unsupported architecture: ${archRaw}" ;; +esac + +log "Installing ${TOOL_NAME} (${VERSION}) for ${os}/${arch} to ${INSTALL_DIR}" + +# GitHub API authentication +ghAuthHeader=(-H "Accept: application/vnd.github+json") +if [[ -n "${GITHUB_TOKEN:-}" ]]; then + ghAuthHeader+=(-H "Authorization: Bearer ${GITHUB_TOKEN}") +fi + +# Create temp directory +tempDir="$(mktemp -d)" || die "Failed to create temp directory" + +# Fetch release info and download URL +log "Fetching release information" +if [[ "${VERSION}" == "latest" ]]; then + apiUrl="https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases/latest" +else + apiUrl="https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases/tags/${VERSION}" +fi + +releaseJson="${tempDir}/release.json" +if ! curl "${ghAuthHeader[@]}" -fsSL "${apiUrl}" -o "${releaseJson}"; then + die "Failed to fetch release information. Check version or network connection." +fi + +# Extract download URL +downloadUrl="$(jq -r --arg os "${os}" --arg arch "${arch}" \ + '.assets[] | select(.browser_download_url | endswith("_\($os)_\($arch)")) | .browser_download_url' \ + "${releaseJson}")" + +[[ -n "${downloadUrl}" ]] || die "No asset found for ${os}/${arch}" + +log "Downloading ${downloadUrl}" +binaryPath="${tempDir}/${TOOL_NAME}" +curl "${ghAuthHeader[@]}" -fsSL "${downloadUrl}" -o "${binaryPath}" || die "Download failed" + +# Install binary +log "Installing binary" +if [[ "${os}" == "darwin" ]]; then + # macOS install command may not support -D flag in all versions + mkdir -p "${INSTALL_DIR}" || die "Failed to create install directory" + install -m 0755 "${binaryPath}" "${INSTALL_DIR}/${TOOL_NAME}" || die "Failed to install binary" +else + # Linux install with -D flag + install -Dm0755 "${binaryPath}" "${INSTALL_DIR}/${TOOL_NAME}" || die "Failed to install binary" +fi + +log "โœ“ Successfully installed ${TOOL_NAME} to ${INSTALL_DIR}/${TOOL_NAME}" + +# Run tool version to verify +"${INSTALL_DIR}/${TOOL_NAME}" --version || die "Installed binary failed to run (${INSTALL_DIR}/${TOOL_NAME})" diff --git a/.taskfiles/scripts/install_terraform.sh b/.taskfiles/scripts/install_terraform.sh new file mode 100755 index 0000000..470eb35 --- /dev/null +++ b/.taskfiles/scripts/install_terraform.sh @@ -0,0 +1,94 @@ +#!/bin/bash + +set -euo pipefail + +# Constants +readonly TOOL_NAME="terraform" +readonly KEYRING_URL="https://apt.releases.hashicorp.com/gpg" +readonly KEYRING_PATH="/usr/share/keyrings/hashicorp-archive-keyring.gpg" +readonly SOURCES_LIST="/etc/apt/sources.list.d/hashicorp.list" + +# Configuration (can be overridden by env) +VERSION="${1:-${VERSION:-latest}}" + +log() { + echo "-> $*" >&2 +} + +die() { + echo "X Error: $*" >&2 + exit "${2:-1}" +} + +usage() { + cat </dev/null 2>&1; then + die "Missing required dependency: ${dep}" + fi +done + +# Download and install GPG key +log "Setting up HashiCorp GPG key" +if ! wget -O - "${KEYRING_URL}" | gpg --batch --yes --dearmor -o "${KEYRING_PATH}"; then + die "Failed to download and install GPG keyring" +fi + +# Add apt repository +log "Adding HashiCorp apt repository" +arch="$(dpkg --print-architecture)" || die "Failed to detect architecture" +codename="$(lsb_release -cs)" || die "Failed to detect distribution codename" + +echo "deb [arch=${arch} signed-by=${KEYRING_PATH}] https://apt.releases.hashicorp.com ${codename} main" \ + | tee "${SOURCES_LIST}" >/dev/null || die "Failed to add apt repository" + +# Update apt cache and install +log "Updating apt cache" +apt-get update || die "Failed to update apt cache" + +log "Installing ${TOOL_NAME}" +if [[ "${VERSION}" == "latest" ]]; then + apt-get install -y terraform || die "Failed to install ${TOOL_NAME}" +else + # Try to install specific version (format: terraform=version) + apt-get install -y "terraform=${VERSION}" || die "Failed to install ${TOOL_NAME} version ${VERSION}" +fi + +log "โœ“ Successfully installed ${TOOL_NAME}" + +# Verify installation +"${TOOL_NAME}" --version || die "Installed binary failed to run" diff --git a/.taskfiles/scripts/setup_fnm.sh b/.taskfiles/scripts/setup_fnm.sh new file mode 100755 index 0000000..8293c6f --- /dev/null +++ b/.taskfiles/scripts/setup_fnm.sh @@ -0,0 +1,138 @@ +#!/bin/bash + +set -euo pipefail + +# Constants +readonly TOOL_NAME="fnm" +readonly INSTALL_SCRIPT_URL="https://fnm.vercel.app/install" + +# Configuration (can be overridden by env) +VERSION="${1:-${VERSION:-latest}}" +INSTALL_DIR="${2:-${INSTALL_DIR:-}}" + +# Logging helper +log() { + echo "-> $*" >&2 +} + +# Error handling helper +die() { + echo "X Error: $*" >&2 + exit "${2:-1}" +} + +# Help message +usage() { + cat < "latest", numeric -> add "v" prefix +if [[ -z "${VERSION//[[:space:]]/}" ]]; then + VERSION="latest" +elif [[ "${VERSION}" != "latest" && "${VERSION}" =~ ^[0-9] ]]; then + VERSION="v${VERSION}" +fi + +# Determine install directory +if [[ -z "${INSTALL_DIR}" ]]; then + INSTALL_DIR="${HOME}/.local/share/fnm" +fi + +# Check dependencies +command -v curl >/dev/null 2>&1 || die "Missing required dependency: curl" + +# Create install directory if it doesn't exist +if [[ ! -d "${INSTALL_DIR}" ]]; then + mkdir -p "${INSTALL_DIR}" || die "Cannot create install directory ${INSTALL_DIR}" +fi + +log "Installing ${TOOL_NAME} (${VERSION}) to ${INSTALL_DIR}" + +# Execute remote installation script +log "Fetching and executing official installation script" +if ! curl -fsSL "${INSTALL_SCRIPT_URL}" | bash -s -- --skip-shell --install-dir "${INSTALL_DIR}" --release "${VERSION}"; then + die "Installation failed. Check version or network connection." +fi + +log "โœ“ Successfully installed ${TOOL_NAME} to ${INSTALL_DIR}" + +# Run tool version to verify +"${INSTALL_DIR}/fnm" --version || die "Installed binary failed to run (${INSTALL_DIR}/fnm)" + +# Create symlink in ~/.local/bin +SYMLINK_DIR="${HOME}/.local/bin" +SYMLINK_PATH="${SYMLINK_DIR}/${TOOL_NAME}" + +if [[ ! -d "${SYMLINK_DIR}" ]]; then + mkdir -p "${SYMLINK_DIR}" || die "Cannot create symlink directory ${SYMLINK_DIR}" +fi + +if [[ -L "${SYMLINK_PATH}" ]]; then + log "Removing existing symlink at ${SYMLINK_PATH}" + rm -f "${SYMLINK_PATH}" +elif [[ -e "${SYMLINK_PATH}" ]]; then + die "Cannot create symlink: ${SYMLINK_PATH} already exists and is not a symlink" +fi + +ln -s "${INSTALL_DIR}/${TOOL_NAME}" "${SYMLINK_PATH}" || die "Failed to create symlink ${SYMLINK_PATH}" +log "โœ“ Created symlink: ${SYMLINK_PATH} -> ${INSTALL_DIR}/${TOOL_NAME}" + +# Function to add environment variables to shell config +add_to_shell() { + local shell_config="$1" + local shell_type="$2" + + if [[ -f "${shell_config}" ]]; then + # Array of environment variables to add + local env_vars=( + "export PATH=\$PATH:${INSTALL_DIR}" + ) + + # Add each variable if not already present + for str in "${env_vars[@]}"; do + if ! grep -qF "${str}" "${shell_config}"; then + echo "${str}" >>"${shell_config}" + fi + done + + # Add fnm env command if not already present + if ! grep -q "fnm env" "${shell_config}"; then + echo "eval \"\$(fnm env --use-on-cd --shell ${shell_type})\"" >>"${shell_config}" + fi + fi +} + +# Configure both bash and zsh if present +add_to_shell ~/.bashrc bash +add_to_shell ~/.zshrc zsh +add_to_shell ~/.config/fish/config.fish fish + +# Verify installation +"${INSTALL_DIR}/${TOOL_NAME}" --version || die "Installed binary failed to run" + +# Since this script runs in bash, only evaluate fnm env for bash +log "Setting up environment for current session" +eval "$("${INSTALL_DIR}/${TOOL_NAME}" env --use-on-cd --shell bash)" diff --git a/.taskfiles/scripts/setup_golang.sh b/.taskfiles/scripts/setup_golang.sh new file mode 100755 index 0000000..7c08cc9 --- /dev/null +++ b/.taskfiles/scripts/setup_golang.sh @@ -0,0 +1,184 @@ +#!/bin/bash + +set -euo pipefail + +# Constants +readonly TOOL_NAME="go" + +# Configuration (can be overridden by env) +VERSION="${1:-${VERSION:-latest}}" +INSTALL_DIR="${2:-${INSTALL_DIR:-}}" + +tempFile="" + +# Logging helper +log() { + echo "-> $*" >&2 +} + +# Error handling helper +die() { + echo "X Error: $*" >&2 + exit "${2:-1}" +} +# Help message +usage() { + cat < "latest" +if [[ -z "${VERSION//[[:space:]]/}" ]]; then + VERSION="latest" +fi + +# Check dependencies +for dep in curl jq wget; do + command -v "${dep}" >/dev/null 2>&1 || die "Missing required dependency: ${dep}" +done + +# Determine Go version to install +if [[ "${VERSION}" != "latest" ]]; then + # Strip 'go' prefix if present and add it back + VERSION="${VERSION#go}" + goVersion="go${VERSION}" +else + log "Fetching latest Go version" + goVersion=$(curl -fsSL "https://go.dev/dl/?mode=json" | jq -r '.[0].version') +fi + +# Detect architecture +archRaw="$(uname -m)" +case "${archRaw}" in + x86_64 | amd64) arch="amd64" ;; + arm64 | aarch64) arch="arm64" ;; + armv7l | armv6l) arch="armv6l" ;; + i386 | i686) arch="386" ;; + *) die "Unsupported architecture: ${archRaw}" ;; +esac + +goFile="${goVersion}.linux-${arch}.tar.gz" + +# Determine install directory and installation mode +if [[ -z "${INSTALL_DIR}" ]]; then + if [[ "${EUID}" -eq 0 ]]; then + INSTALL_DIR="/usr/local" + installMode="system" + else + INSTALL_DIR="${HOME}/.local" + installMode="user" + fi +else + # Custom install dir - determine mode based on permissions + if [[ "${EUID}" -eq 0 ]]; then + installMode="system" + else + installMode="user" + fi +fi + +goInstallPath="${INSTALL_DIR}/go" + +# Create install directory if it doesn't exist +if [[ ! -d "${INSTALL_DIR}" ]]; then + mkdir -p "${INSTALL_DIR}" || die "Cannot create install directory ${INSTALL_DIR}" +fi + +log "Installing ${TOOL_NAME} (${goVersion}) for ${arch} to ${goInstallPath}" + +# Check if Go is already installed and matches the desired version +if [[ -x "${goInstallPath}/bin/go" ]]; then + installedVersion=$("${goInstallPath}/bin/go" version | awk '{print $3}') + if [[ "${installedVersion}" == "${goVersion}" ]]; then + log "โœ“ Go ${goVersion} is already installed. Skipping installation." + exit 0 + else + log "Go ${installedVersion} is installed, but ${goVersion} is required. Updating..." + fi +fi + +# Download Go +log "Downloading ${goFile}" +tempFile="${goFile}" +if ! wget -q "https://golang.org/dl/${goFile}"; then + die "Failed to download Go ${goVersion}" +fi + +# Remove existing installation and extract new one +log "Extracting Go to ${INSTALL_DIR}" +rm -rf "${goInstallPath}" +tar -C "${INSTALL_DIR}" -xzf "${goFile}" || die "Failed to extract Go archive" + +log "โœ“ Successfully installed ${TOOL_NAME} to ${goInstallPath}" + +# Configure environment based on installation mode +if [[ "${installMode}" == "system" ]]; then + # System-wide installation - create profile.d script + if [[ ! -f /etc/profile.d/golang.sh ]]; then + log "Setting up system-wide environment variables" + # shellcheck disable=SC2016 + echo "export PATH=\"\$PATH:${goInstallPath}/bin\"" | tee /etc/profile.d/golang.sh >/dev/null + fi +else + # User installation - add to shell configs + log "Setting up user environment variables" + + # Function to add environment variables to shell config + add_to_shell() { + local shell_config="$1" + + if [[ -f "${shell_config}" ]]; then + # Array of environment variables to add + # shellcheck disable=SC2016 + local env_vars=( + "export PATH=\$PATH:${goInstallPath}/bin" + 'export GOPATH=$HOME/go' + 'export PATH=$PATH:$GOPATH/bin' + ) + + # Add each variable if not already present + for str in "${env_vars[@]}"; do + if ! grep -qF "${str}" "${shell_config}"; then + echo "${str}" >>"${shell_config}" + fi + done + fi + } + + # Configure both bash and zsh if present + add_to_shell ~/.bashrc + add_to_shell ~/.zshrc + + log "Note: Restart your shell or run 'source ~/.bashrc' to use Go" +fi + +# Verify installation +"${goInstallPath}/bin/go" version || die "Installed binary failed to run" diff --git a/.taskfiles/scripts/setup_pwsh.sh b/.taskfiles/scripts/setup_pwsh.sh new file mode 100755 index 0000000..cdaabd5 --- /dev/null +++ b/.taskfiles/scripts/setup_pwsh.sh @@ -0,0 +1,146 @@ +#!/bin/bash + +set -euo pipefail + +# Constants +readonly TOOL_NAME="pwsh" +readonly KEYRING_URL_BASE="https://packages.microsoft.com/config" +readonly PACKAGES_DEB="packages-microsoft-prod.deb" + +# Configuration (can be overridden by env) +VERSION="${1:-${VERSION:-latest}}" + +tempFile="" + +log() { + echo "-> $*" >&2 +} + +die() { + echo "X Error: $*" >&2 + exit "${2:-1}" +} +usage() { + cat < "latest" +if [[ -z "${VERSION//[[:space:]]/}" ]]; then + VERSION="latest" +fi + +os="$(uname -s | tr '[:upper:]' '[:lower:]')" + +if [[ "${os}" == "darwin" ]]; then + log "Installing ${TOOL_NAME} (${VERSION}) using Homebrew" + + command -v brew >/dev/null 2>&1 || die "brew is not installed. Install it from https://brew.sh/" + + if [[ "${VERSION}" != "latest" ]]; then + log "Note: version pinning isn't supported by this Homebrew installer path; installing latest stable." + fi + + brew install --cask powershell || die "Failed to install PowerShell via Homebrew" + + log "โœ“ Successfully installed ${TOOL_NAME}" + pwsh --version >/dev/null 2>&1 || die "Installed binary failed to run" + exit 0 +fi + +if [[ "${os}" != "linux" ]]; then + die "Unsupported OS: ${os}" +fi + +check_sudo + +if [[ "${VERSION}" != "latest" ]]; then + die "This installer uses the Microsoft package repository method and installs the latest available version. For specific versions, use the official direct-download method." +fi + +log "Installing ${TOOL_NAME} (${VERSION}) via Microsoft package repository" + +log "Checking dependencies" +for dep in apt-get wget dpkg; do + command -v "${dep}" >/dev/null 2>&1 || die "Missing required dependency: ${dep}" +done + +log "Detecting distribution" +[[ -f /etc/os-release ]] || die "Missing /etc/os-release" +# shellcheck disable=SC1091 +source /etc/os-release + +case "${ID:-}" in + debian) + distro="debian" + ;; + ubuntu) + distro="ubuntu" + ;; + *) + die "Unsupported Linux distribution: ${ID:-unknown}" + ;; +esac + +[[ -n "${VERSION_ID:-}" ]] || die "Unable to determine VERSION_ID from /etc/os-release" + +log "Updating package lists" +apt-get update || die "Failed to update apt cache" + +log "Installing prerequisites" +apt-get install -y wget || die "Failed to install prerequisites" + +log "Setting up Microsoft repository" +tempFile="${PACKAGES_DEB}" +repoUrl="${KEYRING_URL_BASE}/${distro}/${VERSION_ID}/${PACKAGES_DEB}" + +if ! wget -q "${repoUrl}"; then + die "Failed to download Microsoft repository configuration: ${repoUrl}" +fi + +dpkg -i "${PACKAGES_DEB}" || die "Failed to register Microsoft repository keys" +rm -f "${PACKAGES_DEB}" || true +tempFile="" + +log "Updating package lists (post-repo)" +apt-get update || die "Failed to update apt cache" + +log "Installing PowerShell" +apt-get install -y powershell || die "Failed to install ${TOOL_NAME}" + +log "โœ“ Successfully installed ${TOOL_NAME}" +"${TOOL_NAME}" -Version >/dev/null 2>&1 || die "Installed binary failed to run" diff --git a/.taskfiles/scripts/setup_uv.sh b/.taskfiles/scripts/setup_uv.sh new file mode 100755 index 0000000..7d42ecb --- /dev/null +++ b/.taskfiles/scripts/setup_uv.sh @@ -0,0 +1,88 @@ +#!/bin/bash + +set -euo pipefail + +# Constants +readonly TOOL_NAME="uv" +readonly INSTALL_SCRIPT_URL="https://astral.sh/uv/install.sh" + +# Configuration (can be overridden by env) +VERSION="${1:-${VERSION:-latest}}" +INSTALL_DIR="${2:-${INSTALL_DIR:-}}" + +# Logging helper +log() { + echo "-> $*" >&2 +} + +# Error handling helper +die() { + echo "X Error: $*" >&2 + exit "${2:-1}" +} + +# Help message +usage() { + cat < "latest", strip "v" prefix if present +if [[ -z "${VERSION//[[:space:]]/}" ]]; then + VERSION="latest" +elif [[ "${VERSION}" != "latest" ]]; then + # uv installer expects version without 'v' prefix + VERSION="${VERSION#v}" +fi + +# Determine install directory +if [[ -z "${INSTALL_DIR}" ]]; then + if [[ "${EUID}" -eq 0 ]]; then + INSTALL_DIR="/usr/local/bin" + else + INSTALL_DIR="${HOME}/.local/bin" + fi +fi + +# Check dependencies +command -v curl >/dev/null 2>&1 || die "Missing required dependency: curl" + +# Create install directory if it doesn't exist +if [[ ! -d "${INSTALL_DIR}" ]]; then + mkdir -p "${INSTALL_DIR}" || die "Cannot create install directory ${INSTALL_DIR}" +fi + +log "Installing ${TOOL_NAME} (${VERSION}) to ${INSTALL_DIR}" + +# Execute remote installation script +log "Fetching and executing official installation script" +if ! curl -fsSL "${INSTALL_SCRIPT_URL}" | env UV_INSTALL_DIR="${INSTALL_DIR}" UV_VERSION="${VERSION}" UV_GITHUB_TOKEN="${GITHUB_TOKEN:-}" sh; then + die "Installation failed. Check version or network connection." +fi + +log "โœ“ Successfully installed ${TOOL_NAME} to ${INSTALL_DIR}/uv" + +# Run tool version to verify +"${INSTALL_DIR}/uv" --version || die "Installed binary failed to run (${INSTALL_DIR}/uv)" diff --git a/.taskfiles/shell.Taskfile.yml b/.taskfiles/shell.Taskfile.yml new file mode 100644 index 0000000..238de84 --- /dev/null +++ b/.taskfiles/shell.Taskfile.yml @@ -0,0 +1,102 @@ +# yaml-language-server: $schema=https://taskfile.dev/schema.json +# docs: https://taskfile.dev +--- +version: "3" + +vars: + SHELL_VERSION: + map: + shellcheck: latest + shfmt: latest + +tasks: + # * Install shellcheck + install:shellcheck: + desc: Install shellcheck + cmds: + - task: :internal:_install:winget + vars: + APP: koalaman.shellcheck + VERSION: "{{.SHELL_VERSION.shellcheck}}" + - task: :internal:_install:brew + vars: + APP: shellcheck@{{.SHELL_VERSION.shellcheck}} + - cmd: | + ./scripts/install_shellcheck.sh "{{.SHELL_VERSION.shellcheck}}" + platforms: [linux] + dir: "{{.TASKFILE_DIR}}" + + # * Install shfmt + install:shfmt: + desc: Install shfmt + cmds: + - task: :internal:_install:winget + vars: + APP: mvdan.shfmt + VERSION: "{{.SHELL_VERSION.shfmt}}" + - cmd: | + ./scripts/install_shfmt.sh "{{.SHELL_VERSION.shfmt}}" + platforms: [linux, darwin] + dir: "{{.TASKFILE_DIR}}" + + # * Tools + tools:lint: + desc: Install Shell tools + aliases: + - tools + vars: + ITEMS: + - shellcheck + - shfmt + cmds: + - for: { var: ITEMS } + task: install:{{.ITEM}} + + # * Run shfmt + run:shfmt: + desc: Run shfmt + preconditions: + - sh: task internal:command -- shfmt + msg: "โŒ Error: shfmt is not installed. Please run 'task sh:install:shfmt" + - sh: task internal:command:version -- "shfmt --version" "{{.SHELL_VERSION.shfmt}}" + msg: "โŒ Version Mismatch: You are not using {{.SHELL_VERSION.shfmt}} version. Please run 'task sh:install:shfmt'" + vars: + SHELL_FILES: + sh: | + {{if eq OS "windows"}} + {{.PWSH}} 'shfmt --find . | Where-Object { $_ -notmatch "\\(.git|node_modules|\\.terraform|\\.venv|\\.dev)\\" }' + {{else}} + shfmt --find . | grep -v -E '\./\.git/|\./node_modules/|/\.terraform/|\.terraform/|\./\.venv/|\./\.dev/' + {{end}} + cmds: + - for: { var: SHELL_FILES } + cmd: shfmt -w -i 2 -ci -bn "{{osClean .ITEM}}" + dir: "{{.USER_WORKING_DIR}}" + + # * Run shellcheck + run:shellcheck: + desc: Run shellcheck + preconditions: + - sh: task internal:command -- shellcheck + msg: "โŒ Error: shellcheck is not installed. Please run 'task sh:install:shellcheck'" + - sh: task internal:command:version -- "shellcheck --version" "{{.SHELL_VERSION.shellcheck}}" + msg: "โŒ Version Mismatch: You are not using {{.SHELL_VERSION.shellcheck}} version. Please run 'task sh:install:shellcheck'" + vars: + ITEMS: + sh: | + {{if eq OS "windows"}} + {{.PWSH}} 'Get-ChildItem -Path . -Filter "*.sh" -Recurse | Where-Object { $_.FullName -notmatch "\\(.git|node_modules|.terraform|.venv|.dev)\\" } | ForEach-Object { $_.FullName | Resolve-Path -Relative }' + {{else}} + find . -type f -name "*.sh" -not -path "./.git/*" -not -path "./node_modules/*" -not -path "*/.venv/*" -not -path "*/.dev/*" -not -path "*/.terraform/*" -not -path ".terraform/*" + {{end}} + cmds: + - for: { var: ITEMS } + cmd: shellcheck "{{osClean .ITEM}}" + dir: "{{.USER_WORKING_DIR}}" + + # * Lint + lint: + desc: Lint Shell files + cmds: + - task: run:shfmt + - task: run:shellcheck diff --git a/.taskfiles/terraform.Taskfile.yml b/.taskfiles/terraform.Taskfile.yml new file mode 100644 index 0000000..699df31 --- /dev/null +++ b/.taskfiles/terraform.Taskfile.yml @@ -0,0 +1,93 @@ +# yaml-language-server: $schema=https://taskfile.dev/schema.json +# docs: https://taskfile.dev +--- +version: "3" + +vars: + TERRAFORM_VERSION: + map: + terraform: latest + opentofu: latest + +tasks: + # * Install Terraform + install:terraform: + desc: Install Terraform + cmds: + - task: :internal:_install:winget + vars: + APP: HashiCorp.Terraform + VERSION: "{{.TERRAFORM_VERSION.terraform}}" + - task: :internal:_install:brew:tap + vars: + APP: hashicorp/tap + - task: :internal:_install:brew + vars: + APP: hashicorp/tap/terraform@{{.TERRAFORM_VERSION.terraform}} + - cmd: | + sudo ./scripts/install_terraform.sh "{{.TERRAFORM_VERSION.terraform}}" + platforms: [linux] + dir: "{{.TASKFILE_DIR}}" + + # * Install OpenTofu + install:opentofu: + desc: Install OpenTofu + cmds: + - task: :internal:_install:winget + vars: + APP: OpenTofu.Tofu + VERSION: "{{.TERRAFORM_VERSION.opentofu}}" + - task: :internal:_install:brew + vars: + APP: opentofu@{{.TERRAFORM_VERSION.opentofu}} + - cmd: | + ./scripts/install_opentofu.sh "{{.TERRAFORM_VERSION.opentofu}}" + platforms: [linux] + dir: "{{.TASKFILE_DIR}}" + + # * Tools + tools:lint: + desc: Install Terraform tools + aliases: + - tools + vars: + ITEMS: + - terraform + - opentofu + cmds: + - for: { var: ITEMS } + task: install:{{.ITEM}} + + # * Run Terraform fmt + run:terraform-fmt: + desc: Run terraform fmt + preconditions: + - sh: task internal:command -- terraform + msg: "โŒ Error: terraform is not installed. Please run 'task tf:install:terraform'" + - sh: task internal:command:version -- "terraform --version" "{{.TERRAFORM_VERSION.terraform}}" + msg: "โŒ Version Mismatch: You are not using {{.TERRAFORM_VERSION.terraform}} version. Please run 'task tf:install:terraform'" + sources: + - "**/*.tf" + - "**/*.tfvars" + - "**/*.hcl" + cmds: + - terraform fmt -recursive + dir: "{{.ROOT_DIR}}" + + # * Lint + lint: + desc: Lint Terraform files + cmds: + - task: run:terraform-fmt + + # * Clean + clean: + desc: Clean up Terraform files + cmds: + - cmd: | + find ./ -type d \( -name ".external_modules" -o -name ".terraform" \) -exec rm -rf {} + 2>/dev/null; find ./ -type f \( -name "*.terraform.lock.*" -o -name "*.tfstate*" -o -name "terraform.log" \) -delete 2>/dev/null; true + platforms: [linux, darwin] + - cmd: | + {{.PWSH}} 'Get-ChildItem -Path ./ -Include ".external_modules",".terraform" -Directory -Recurse -ErrorAction SilentlyContinue | Remove-Item -Recurse -Force; Get-ChildItem -Path ./ -Include "*.terraform.lock.*","*.tfstate*","terraform.log" -File -Recurse -ErrorAction SilentlyContinue | Remove-Item -Force' + platforms: [windows] + dir: "{{.ROOT_DIR}}" diff --git a/.taskfiles/yaml.Taskfile.yml b/.taskfiles/yaml.Taskfile.yml new file mode 100644 index 0000000..cd6c983 --- /dev/null +++ b/.taskfiles/yaml.Taskfile.yml @@ -0,0 +1,51 @@ +# yaml-language-server: $schema=https://taskfile.dev/schema.json +# docs: https://taskfile.dev +--- +version: "3" + +vars: + YAML_VERSION: + map: + yamllint: latest + +tasks: + # * Install YAMLlint + install:yamllint: + desc: Install YAMLlint + cmds: + - task: :internal:_install:uv + vars: + APP: yamllint@{{.YAML_VERSION.yamllint}} + + # * Tools + tools:lint: + desc: Install YAML tools (lint) + aliases: + - tools + vars: + ITEMS: + - yamllint + cmds: + - for: { var: ITEMS } + task: install:{{.ITEM}} + + # * Run YAMLlint + run:yamllint: + desc: Run YAMLlint + preconditions: + - sh: task internal:command -- yamllint + msg: "โŒ Error: yamllint is not installed. Please run 'task yml:install:yamllint'" + - sh: task internal:command:version -- "yamllint --version" "{{.YAML_VERSION.yamllint}}" + msg: "โŒ Version Mismatch: You are not using {{.YAML_VERSION.yamllint}} version. Please run 'task yml:install:yamllint'" + sources: + - "**/*.yml" + - "**/*.yaml" + cmds: + - yamllint --config-file ./.github/linters/.yamllint.yml --format auto --strict . + dir: "{{.ROOT_DIR}}" + + # * Lint + lint: + desc: Lint YAML files + cmds: + - task: run:yamllint diff --git a/.vscode/settings.json b/.vscode/settings.json index b31e31b..e8b40d2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,58 @@ { + // github + "github.branchProtection": true, + "githubPullRequests.showPullRequestNumberInTree": true, + // javascript + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + // typescript + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + // json + "[json]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[jsonc]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "prettier.configPath": "./.github/linters/.prettierrc.yml", + // yaml + "[yaml]": { + "editor.defaultFormatter": "redhat.vscode-yaml" + }, + "yaml.format.singleQuote": false, + "yaml.schemaStore.enable": true, + // markdown + "[markdown]": { + "editor.defaultFormatter": "DavidAnson.vscode-markdownlint", + "editor.formatOnSave": true, + "editor.formatOnPaste": true + }, + "markdownlint.configFile": "./.github/linters/.markdownlint.yml", + "markdown.extension.toc.levels": "2..6", + "markdown.extension.toc.updateOnSave": true, + "markdown.extension.tableFormatter.normalizeIndentation": true, + "markdown.extension.tableFormatter.delimiterRowNoPadding": true, + // shell + "shellcheck.customArgs": [ + "--rcfile", + "./.github/linters/.shellcheckrc" + ], + "shellcheck.enable": true, + "shellcheck.enableQuickFix": true, + "shellcheck.useWorkspaceRootAsCwd": true, + "shellformat.useEditorConfig": true, + // general lint + "linter.linters": { + "yamllint": { + "configFiles": ["./.github/linters/.yamllint.yml"] + }, + "markdownlint": { + "configFiles": ["./.github/linters/.markdownlint.yml"] + } + }, + // golang "go.testEnvFile": "${workspaceFolder}/dev.env" } diff --git a/.yamlignore b/.yamlignore new file mode 100644 index 0000000..1c3da07 --- /dev/null +++ b/.yamlignore @@ -0,0 +1,5 @@ + **/.terraform/** + **/.venv/** + **/venv/** + **/.git/** + **/node_modules/** diff --git a/Taskfile.yml b/Taskfile.yml index 5dc1767..a256393 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -3,16 +3,82 @@ --- version: "3" -dotenv: [dev.env] +set: [pipefail] +shopt: [globstar] + +dotenv: [.env, dev.env, ".{{.ENV}}/.env", "{{.ENV}}/.env"] vars: PROJECT_NAME: azmpf - BUILD_DEV_OUTPUT_DIR: "bin/{{OS}}-{{ARCH}}" + BUILD_DEV_OUTPUT_DIR: bin/{{OS}}-{{ARCH}} BUILD_DEV_ARTIFACT: "{{.BUILD_DEV_OUTPUT_DIR}}/{{.PROJECT_NAME}}{{exeExt}}" - PWSH: pwsh -NoProfile -NonInteractive -NoLogo -Command - PWSH_SCRIPT: pwsh -NoProfile -NonInteractive -NoLogo -ExecutionPolicy Bypass -File + +includes: + internal: .taskfiles/_internal.Taskfile.yml + runtime: .taskfiles/runtime.Taskfile.yml + az: .taskfiles/azure.Taskfile.yml + gh: .taskfiles/github.Taskfile.yml + sh: .taskfiles/shell.Taskfile.yml + go: .taskfiles/golang.Taskfile.yml + tf: .taskfiles/terraform.Taskfile.yml + md: .taskfiles/markdown.Taskfile.yml + yml: .taskfiles/yaml.Taskfile.yml tasks: + default: + desc: List available tasks + silent: true + cmds: + - task --list + + sysinfo: + desc: Display system information + silent: true + cmds: + # yamllint disable-line rule:quoted-strings + - 'echo "OS: {{OS}}"' # windows, linux, darwin + # yamllint disable-line rule:quoted-strings + - 'echo "Architecture: {{ARCH}}"' # amd64, arm64, etc. + # yamllint disable-line rule:quoted-strings + - 'echo "CPU cores: {{numCPU}}"' # Number of CPU cores + + tools: + desc: Install tools + vars: + ITEMS: + - az + - gh + - sh + - go + - tf + - md + - yml + cmds: + - for: { var: ITEMS } + task: "{{.ITEM}}:tools" + + lint: + desc: Lint files + vars: + ITEMS: + - gh + - sh + - go + - tf + - md + - yml + deps: + - for: { var: ITEMS } + task: "{{.ITEM}}:lint" + + init: + desc: Initialize development environment + silent: true + cmds: + - task: runtime:setup + - task: tools + dir: "{{.USER_WORKING_DIR}}" + deps: desc: Check if dependencies are up to date cmds: @@ -36,81 +102,6 @@ tasks: - defer: task: deps - lint: - desc: Run linters - cmds: - - task: lint:files - - task: lint:go - - task: lint:tf - - task: lint:md - - lint:files: - desc: Run linters for various file types - cmds: - - copywrite headers - - copywrite license - - lint:go: - desc: Run Go linters - cmds: - - task: govulncheck - - task: golangci-lint - - govulncheck: - desc: Run govulncheck - cmds: - - govulncheck -test -show verbose ./... - - golangci-lint: - desc: Run golangci-lint - cmds: - - golangci-lint run --fix ./... - - lint:tf: - desc: Run Terraform linters - cmds: - - terraform fmt -recursive - - tflint --recursive - - tfsec . - - checkov --directory . - - lint:md: - desc: Run Markdown linters - cmds: - - markdownlint-cli2 "./**/*.md" --config "./.github/linters/.markdownlint-cli2.yaml" --fix - - lint:links: - desc: Run link checkers - cmds: - - lychee --config ./.github/linters/.lychee.toml --format markdown . - - tools: - desc: Install required tools - cmds: - - for: - [ - semver, - copywrite, - changie, - dlv, - goimports, - golangci-lint, - gofumpt, - goreleaser, - govulncheck, - yamllint, - markdownlint, - ] - task: install:{{.ITEM}} - - task: test:tools - - task: lint:tf-tools - - lint:tf-tools: - desc: Install Terraform lint tools - cmds: - - for: [tflint, tfsec, checkov] - task: install:{{.ITEM}} - # ---------------------- # Build # ---------------------- @@ -212,6 +203,7 @@ tasks: _teste2e:run: internal: true vars: + # yamllint disable-line rule:quoted-strings FORMAT: '{{if eq .GITHUB_ACTIONS "true"}}github-actions{{else}}pkgname-and-test-fails{{end}}' env: MPF_TFPATH: "{{.MPF_TFPATH}}" @@ -223,28 +215,32 @@ tasks: testunit: desc: Run unit tests cmds: + # yamllint disable-line rule:quoted-strings - 'gotestsum --format-hivis --format {{.FORMAT}} --jsonfile "testresults.json" ./pkg/domain ./pkg/infrastructure/ARMTemplateShared ./pkg/infrastructure/mpfSharedUtils ./pkg/infrastructure/authorizationCheckers/terraform -p {{numCPU}} -timeout 5m -ldflags="{{.LDFLAGS}}" -coverprofile="coverage.out" -covermode atomic' - task: _test:getcover vars: TEST_NAME: "{{if gt (len (splitArgs .CLI_ARGS)) 0}}{{index (splitArgs .CLI_ARGS) 0}}{{end}}" TEST_PATH: "{{if gt (len (splitArgs .CLI_ARGS)) 1}}{{index (splitArgs .CLI_ARGS) 1}}{{else}}./...{{end}}" + # yamllint disable-line rule:quoted-strings FORMAT: '{{if eq .GITHUB_ACTIONS "true"}}github-actions{{else}}pkgname-and-test-fails{{end}}' - LDFLAGS: "-s -w -X main.version=testUnit" + LDFLAGS: -s -w -X main.version=testUnit testcli:arm: desc: Run CLI tests for ARM vars: + # yamllint disable-line rule:quoted-strings EXPECTED: '{{if eq OS "windows"}}13{{else}}14{{end}}' cmds: - task: _testcli:validate vars: - CMD: "go run ./cmd arm --templateFilePath ./samples/templates/aks-private-subnet.json --parametersFilePath ./samples/templates/aks-private-subnet-parameters.json" + CMD: go run ./cmd arm --templateFilePath ./samples/templates/aks-private-subnet.json --parametersFilePath ./samples/templates/aks-private-subnet-parameters.json EXPECTED: "{{.EXPECTED}}" PRE_CMD: "" testcli:bicep: desc: Run CLI tests for Bicep vars: + # yamllint disable-line rule:quoted-strings EXPECTED: '{{if eq OS "windows"}}13{{else}}14{{end}}' MPF_BICEPEXECPATH: sh: | @@ -258,13 +254,14 @@ tasks: cmds: - task: _testcli:validate vars: - CMD: "go run ./cmd bicep --bicepFilePath ./samples/bicep/aks-private-subnet.bicep --parametersFilePath ./samples/bicep/aks-private-subnet-params.json --bicepExecPath \"{{.MPF_BICEPEXECPATH}}\"" + CMD: go run ./cmd bicep --bicepFilePath ./samples/bicep/aks-private-subnet.bicep --parametersFilePath ./samples/bicep/aks-private-subnet-params.json --bicepExecPath "{{.MPF_BICEPEXECPATH}}" EXPECTED: "{{.EXPECTED}}" PRE_CMD: "" testcli:terraform: desc: Run CLI tests for Terraform vars: + # yamllint disable-line rule:quoted-strings EXPECTED: '{{if eq OS "windows"}}12{{else}}13{{end}}' MPF_TFPATH: sh: | @@ -278,17 +275,10 @@ tasks: cmds: - task: _testcli:validate vars: - CMD: "go run ./cmd terraform --workingDir ./samples/terraform/aci --varFilePath ./samples/terraform/aci/dev.vars.tfvars --tfPath \"{{.MPF_TFPATH}}\"" + CMD: go run ./cmd terraform --workingDir ./samples/terraform/aci --varFilePath ./samples/terraform/aci/dev.vars.tfvars --tfPath "{{.MPF_TFPATH}}" EXPECTED: "{{.EXPECTED}}" PRE_CMD: | - {{if eq OS "windows"}} - if (-not $env:USERPROFILE) { $env:USERPROFILE = "C:\Users\runneradmin" } - if (-not $env:HOMEDRIVE) { $env:HOMEDRIVE = "C:" } - if (-not $env:HOMEPATH) { $env:HOMEPATH = "\Users\runneradmin" } - Write-Host "Terraform path: $env:MPF_TFPATH" - {{else}} echo "Terraform path: $MPF_TFPATH" - {{end}} teste2e:arm: desc: Run E2E tests for ARM @@ -332,27 +322,31 @@ tasks: desc: Run acceptance tests cmds: - go clean -testcache + # yamllint disable-line rule:quoted-strings - 'gotestsum --format-hivis --format {{.FORMAT}} --jsonfile "testresults.json" -- {{.TEST_PATH}} -run "^TestAcc_{{.TEST_NAME}}" -p {{numCPU}} -timeout 30m -ldflags="{{.LDFLAGS}}" -coverprofile="coverage.out" -covermode atomic' - task: _test:getcover vars: TEST_NAME: "{{if gt (len (splitArgs .CLI_ARGS)) 0}}{{index (splitArgs .CLI_ARGS) 0}}{{end}}" TEST_PATH: "{{if gt (len (splitArgs .CLI_ARGS)) 1}}{{index (splitArgs .CLI_ARGS) 1}}{{else}}./...{{end}}" + # yamllint disable-line rule:quoted-strings FORMAT: '{{if eq .GITHUB_ACTIONS "true"}}github-actions{{else}}pkgname-and-test-fails{{end}}' - LDFLAGS: "-s -w -X main.version=testAcc" + LDFLAGS: -s -w -X main.version=testAcc test: desc: Run tests cmds: - go clean -testcache - go test -failfast -run ^TestDevEnv_WellKnown$ ./internal/testhelp + # yamllint disable-line rule:quoted-strings - 'gotestsum --format-hivis --format {{.FORMAT}} --jsonfile "testresults.json" -- {{.TEST_PATH}} -run "^Test(Acc|Unit)_{{.TEST_NAME}}" -p {{numCPU}} -timeout 30m -ldflags="{{.LDFLAGS}}" -coverprofile="coverage.out" -covermode atomic -coverpkg={{.GO_PKGS}}' - task: _test:getcover vars: TEST_NAME: "{{if gt (len (splitArgs .CLI_ARGS)) 0}}{{index (splitArgs .CLI_ARGS) 0}}{{end}}" TEST_PATH: "{{if gt (len (splitArgs .CLI_ARGS)) 1}}{{index (splitArgs .CLI_ARGS) 1}}{{else}}./...{{end}}" + # yamllint disable-line rule:quoted-strings FORMAT: '{{if eq .GITHUB_ACTIONS "true"}}github-actions{{else}}pkgname-and-test-fails{{end}}' - LDFLAGS: "-s -w -X main.version=testAcc" - GO_PKGS_EXCLUDE: "/testhelp|/fakes|/terraform-provider-fabric" + LDFLAGS: -s -w -X main.version=testAcc + GO_PKGS_EXCLUDE: /testhelp|/fakes|/terraform-provider-fabric GO_PKGS: sh: | {{if eq OS "windows"}} @@ -364,25 +358,6 @@ tasks: TF_LOG: error TF_ACC: 1 - testacc:setup: - desc: Setup acceptence/development test environment - dotenv: ["wellknown.env"] - preconditions: - - sh: | - {{if ne OS "windows"}} - command -v pwsh &>/dev/null || exit 1 - {{end}} - msg: "First install PowerShell: https://learn.microsoft.com/powershell/scripting/install/installing-powershell" - cmds: - - cmd: | - {{.PWSH_SCRIPT}} ./tools/scripts/Set-WellKnown.ps1 - - test:tools: - desc: Install test tools - cmds: - - for: [gotestsum, gocover-cobertura] - task: install:{{.ITEM}} - _test:getcover: desc: Get coverage results internal: true @@ -404,7 +379,7 @@ tasks: sh: semver up beta env: - CHANGIE_GITHUB_REPOSITORY: "microsoft/terraform-provider-fabric" + CHANGIE_GITHUB_REPOSITORY: microsoft/terraform-provider-fabric cmds: - echo "{{.SEMVER}}" - cmd: | @@ -419,190 +394,25 @@ tasks: - cmd: | gh pr create --title "feat(release): {{.SEMVER}}" --body-file ".changes/{{.SEMVER}}.md" --label "skip-changelog" - # ---------------------- - # Install Helpers - # ---------------------- - install:bicep: - desc: Install Bicep - cmds: - - cmd: | - curl -Lo bicep https://github.com/Azure/bicep/releases/latest/download/bicep-linux-x64 - chmod +x ./bicep - sudo mv ./bicep /usr/local/bin/bicep - platforms: [linux] - - cmd: | - curl -Lo bicep https://github.com/Azure/bicep/releases/latest/download/bicep-osx-x64 - chmod +x ./bicep - sudo spctl --add ./bicep - sudo mv ./bicep /usr/local/bin/bicep - platforms: [darwin] - - cmd: | - {{.PWSH}} ' - $installPath = "$env:USERPROFILE\.bicep" - $installDir = New-Item -ItemType Directory -Path $installPath -Force - $installDir.Attributes += "Hidden" - (New-Object Net.WebClient).DownloadFile("https://github.com/Azure/bicep/releases/latest/download/bicep-win-x64.exe", "$installPath\bicep.exe") - $currentPath = (Get-Item -path "HKCU:\Environment" ).GetValue("Path", "", "DoNotExpandEnvironmentNames") - if (-not $currentPath.Contains("%USERPROFILE%\.bicep")) { setx PATH ($currentPath + ";%USERPROFILE%\.bicep") } - if (-not $env:path.Contains($installPath)) { $env:path += ";$installPath" } - if ($env:GITHUB_PATH) { - echo "$installPath" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - } - ' - platforms: [windows] - - install:semver: - desc: Install semver - cmds: - - go install github.com/maykonlsf/semver-cli/cmd/semver@latest - - install:copywrite: - desc: Install copywrite - cmds: - - go install github.com/hashicorp/copywrite@latest - - install:changie: - desc: Install changie - cmds: - - go install github.com/miniscruff/changie@latest - - install:gotestsum: - desc: Install gotestsum - cmds: - - go install gotest.tools/gotestsum@latest - - install:dlv: - desc: Install dlv - cmds: - - go install github.com/go-delve/delve/cmd/dlv@latest - - install:gocover-cobertura: - desc: Install gocover-cobertura - cmds: - - go install github.com/boumenot/gocover-cobertura@latest - - install:tfplugindocs: - desc: Install tfplugindocs - cmds: - - go install github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs@latest - - install:goimports: - desc: Install goimports - cmds: - - go install golang.org/x/tools/cmd/goimports@latest - - install:golangci-lint: - desc: Install golangci-lint - cmds: - - cmd: winget install GolangCI.golangci-lint - ignore_error: true - platforms: [windows] - - cmd: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b "$(go env GOPATH)/bin" latest - platforms: [linux] - - cmd: brew install golangci-lint - platforms: [darwin] - - install:gofumpt: - desc: Install gofumpt - cmds: - - go install mvdan.cc/gofumpt@latest - - install:goreleaser: - desc: Install goreleaser - cmds: - - cmd: | - echo 'deb [trusted=yes] https://repo.goreleaser.com/apt/ /' | sudo tee /etc/apt/sources.list.d/goreleaser.list - sudo apt update - sudo apt install goreleaser - platforms: [linux] - - cmd: brew install goreleaser - platforms: [darwin] - - cmd: winget install goreleaser.goreleaser - platforms: [windows] - - install:tfproviderlintx: - desc: Install tfproviderlintx - cmds: - - go install github.com/bflad/tfproviderlint/cmd/tfproviderlintx@latest - - install:markdownlint: - desc: Install markdownlint - cmds: - - npm install -g markdownlint-cli2 - - install:tflint: - desc: Install tflint - cmds: - - go install github.com/terraform-linters/tflint@latest - - install:tfsec: - desc: Install tfsec - cmds: - - go install github.com/aquasecurity/tfsec/cmd/tfsec@latest - - install:govulncheck: - desc: Install govulncheck - cmds: - - go install golang.org/x/vuln/cmd/govulncheck@latest - - install:checkov: - desc: Install checkov - preconditions: - - sh: | - {{if eq OS "windows"}} - {{.PWSH}} 'if (-not (Get-Command pipx -ErrorAction SilentlyContinue)) { exit 1 }' - {{else}} - command -v pipx &>/dev/null || exit 1 - {{end}} - msg: "First install pipx: https://pipx.pypa.io/" - cmds: - - pipx install --force checkov - - install:mkdocs: - desc: Install mkdocs - preconditions: - - sh: | - {{if eq OS "windows"}} - {{.PWSH}} 'if (-not (Get-Command pipx -ErrorAction SilentlyContinue)) { exit 1 }' - {{else}} - command -v pipx &>/dev/null || exit 1 - {{end}} - msg: "First install pipx: https://pipx.pypa.io/" - cmds: - - pipx install --force mkdocs - - pipx inject --include-deps --force mkdocs $(mkdocs get-deps) - - install:yamllint: - desc: Install yamllint - preconditions: - - sh: | - {{if eq OS "windows"}} - {{.PWSH}} 'if (-not (Get-Command pipx -ErrorAction SilentlyContinue)) { exit 1 }' - {{else}} - command -v pipx &>/dev/null || exit 1 - {{end}} - msg: "First install pipx: https://pipx.pypa.io/" - cmds: - - pipx install --force yamllint - - install:lychee: - desc: Install lychee - cmds: - - cmd: winget install lycheeverse.lychee - platforms: [windows] - - cmd: cargo install lychee - ignore_error: true - platforms: [linux] - - cmd: brew install lychee - platforms: [darwin] - - tf:clean: - desc: Clean up Terraform files + diff: + desc: Check for differences + silent: true + status: + - git diff --shortstat --exit-code cmds: - cmd: | - find ./ -type d \( -name ".external_modules" -o -name ".terraform" \) -exec rm -rf {} + 2>/dev/null; find ./ -type f \( -name "*.terraform.lock.*" -o -name "*.tfstate*" -o -name "terraform.log" \) -delete 2>/dev/null; true - platforms: [linux, darwin] - - cmd: | - {{.PWSH}} 'Get-ChildItem -Path ./ -Include ".external_modules",".terraform" -Directory -Recurse -ErrorAction SilentlyContinue | Remove-Item -Recurse -Force; Get-ChildItem -Path ./ -Include "*.terraform.lock.*","*.tfstate*","terraform.log" -File -Recurse -ErrorAction SilentlyContinue | Remove-Item -Force' - platforms: [windows] - dir: "{{.ROOT_DIR}}" + echo "โš ๏ธโš ๏ธโš ๏ธ Unexpected difference. Run 'task {{.CLI_ARGS}}' command and commit. โš ๏ธโš ๏ธโš ๏ธ" + echo + echo "๐Ÿ›‘๐Ÿ›‘๐Ÿ›‘ Uncommitted changes ๐Ÿ›‘๐Ÿ›‘๐Ÿ›‘" + echo + git status --short + echo + echo "๐Ÿ“๐Ÿ“๐Ÿ“ Summary ๐Ÿ“๐Ÿ“๐Ÿ“" + echo + git diff --compact-summary + echo + echo "๐Ÿ”๐Ÿ”๐Ÿ” Differences ๐Ÿ”๐Ÿ”๐Ÿ”" + echo + git --no-pager diff + echo + exit 1 diff --git a/e2eTests/e2eTerraform_test.go b/e2eTests/e2eTerraform_test.go index 1984a93..ea1fc12 100644 --- a/e2eTests/e2eTerraform_test.go +++ b/e2eTests/e2eTerraform_test.go @@ -170,8 +170,19 @@ func TestTerraformModuleTest(t *testing.T) { t.Error(err) } + // Microsoft.OperationalInsights/workspaces/delete + // Microsoft.OperationalInsights/workspaces/read + // Microsoft.OperationalInsights/workspaces/write + // Microsoft.Resources/deployments/read + // Microsoft.Resources/deployments/write + // Microsoft.Resources/subscriptions/resourcegroups/delete + // Microsoft.Resources/subscriptions/resourcegroups/read + // Microsoft.Resources/subscriptions/resourcegroups/write + assert.NotEmpty(t, mpfResult.RequiredPermissions) - assert.Equal(t, 8, len(mpfResult.RequiredPermissions[mpfConfig.SubscriptionID])) + perms := mpfResult.RequiredPermissions[mpfConfig.SubscriptionID] + log.Infof("Found %d permissions: %v", len(perms), perms) + assert.Equal(t, 8, len(perms)) } // diff --git a/pkg/infrastructure/spRoleAssignmentManager/defaultSPRoleAssignmentManager.go b/pkg/infrastructure/spRoleAssignmentManager/defaultSPRoleAssignmentManager.go index 0bd3777..b2c58d2 100644 --- a/pkg/infrastructure/spRoleAssignmentManager/defaultSPRoleAssignmentManager.go +++ b/pkg/infrastructure/spRoleAssignmentManager/defaultSPRoleAssignmentManager.go @@ -357,6 +357,7 @@ func (r *SPRoleAssignmentManager) DeleteCustomRole(subscription string, role dom if err != nil { return err } + defer resp.Body.Close() //nolint:errcheck // read response body body, err := io.ReadAll(resp.Body) @@ -364,6 +365,10 @@ func (r *SPRoleAssignmentManager) DeleteCustomRole(subscription string, role dom log.Warnf("Could not delete role definition: %s\n", err) } + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("failed to delete role definition. Status: %s, Body: %s", resp.Status, string(body)) + } + log.Debugln(string(body)) log.Infoln("Role definition deleted successfully") diff --git a/pkg/usecase/mpfService.go b/pkg/usecase/mpfService.go index a373535..c5229b6 100644 --- a/pkg/usecase/mpfService.go +++ b/pkg/usecase/mpfService.go @@ -25,6 +25,7 @@ package usecase import ( "context" "strings" + "time" "github.com/Azure/mpf/pkg/domain" log "github.com/sirupsen/logrus" @@ -104,6 +105,11 @@ func (s *MPFService) GetMinimumPermissionsRequired() (domain.MPFResult, error) { } log.Info("Deleted all existing role assignments for service principal \n") + // Wait for Azure RBAC propagation after deleting role assignments + // This ensures that any previous permissions are fully revoked before starting the new test + log.Infoln("Waiting for Azure RBAC propagation after deleting role assignments...") + time.Sleep(15 * time.Second) + // Initialize new custom role log.Infoln("Initializing Custom Role") // err = mpf.CreateUpdateCustomRole([]string{}) @@ -128,6 +134,11 @@ func (s *MPFService) GetMinimumPermissionsRequired() (domain.MPFResult, error) { } log.Infoln("New Custom Role assigned to service principal successfully") + // Wait for Azure RBAC propagation after initial role assignment + // Azure role assignments can take a few seconds to propagate across all authorization endpoints + log.Infoln("Waiting for Azure RBAC propagation after initial role assignment...") + time.Sleep(5 * time.Second) + // Add initial permissions to requiredPermissions map log.Infoln("Adding initial permissions to requiredPermissions map") s.requiredPermissions[s.mpfConfig.SubscriptionID] = append(s.requiredPermissions[s.mpfConfig.SubscriptionID], s.permissionsToAddToResult...) @@ -206,6 +217,11 @@ func (s *MPFService) GetMinimumPermissionsRequired() (domain.MPFResult, error) { } log.Infoln("Permission/scope added to role successfully") + // Wait for Azure RBAC propagation before retrying deployment + // Azure role definition updates can take a few seconds to propagate across all authorization endpoints + log.Infoln("Waiting for Azure RBAC propagation...") + time.Sleep(5 * time.Second) + iterCount++ if iterCount == maxIterations { log.Warnln("max iterations for fetching authorization errors reached, exiting...") diff --git a/samples/terraform/rg-invalid-tfvars/dev.vars.tfvars b/samples/terraform/rg-invalid-tfvars/dev.vars.tfvars index 3c56856..1778e7c 100644 --- a/samples/terraform/rg-invalid-tfvars/dev.vars.tfvars +++ b/samples/terraform/rg-invalid-tfvars/dev.vars.tfvars @@ -1 +1 @@ -location = +location = "" diff --git a/scripts/cleanup-tmp-roles.ps1 b/scripts/cleanup-tmp-roles.ps1 new file mode 100644 index 0000000..35a4dd1 --- /dev/null +++ b/scripts/cleanup-tmp-roles.ps1 @@ -0,0 +1,59 @@ +Param( + [string]$SubscriptionId = $env:SUBSCRIPTION_ID, + [string]$RolePrefix = $(if ($env:TMP_ROLE_PREFIX) { $env:TMP_ROLE_PREFIX } else { 'tmp-rol-' }) +) + +$ErrorActionPreference = 'Stop' + +if (-not (Get-Command az -ErrorAction SilentlyContinue)) { + Write-Error 'Azure CLI (az) is required' + exit 1 +} + +if ([string]::IsNullOrWhiteSpace($SubscriptionId)) { + try { + $SubscriptionId = az account show --query id -o tsv 2>$null + } + catch { + $SubscriptionId = '' + } +} + +if ([string]::IsNullOrWhiteSpace($SubscriptionId)) { + Write-Error 'Subscription not provided and no default found. Set SUBSCRIPTION_ID or login.' + exit 1 +} + +az account set --subscription $SubscriptionId | Out-Null + +$query = "[?starts_with(roleName, '$RolePrefix')].{Id:id,Name:name,RoleName:roleName}" +$defs = az role definition list --custom-role-only true --subscription $SubscriptionId --query $query -o json | ConvertFrom-Json + +if (-not $defs -or $defs.Count -eq 0) { + Write-Host 'No tmp-rol* custom roles found' + exit 0 +} + +foreach ($def in $defs) { + $roleId = $def.Id + $roleName = $def.Name + $roleDisplay = $def.RoleName + + Write-Host "Processing role $roleDisplay ($roleName)" + + $assignments = az role assignment list --all --subscription $SubscriptionId --role $roleId --query '[].id' -o tsv + if ([string]::IsNullOrWhiteSpace($assignments)) { + Write-Host "No assignments for $roleDisplay" + } + else { + $assignments -split "`n" | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | ForEach-Object { + Write-Host "Deleting assignment $_" + az role assignment delete --ids $_ + } + } + + Write-Host "Deleting role definition $roleName" + az role definition delete --name $roleName --subscription $SubscriptionId + Write-Host "Done $roleDisplay" + Write-Host +} diff --git a/scripts/cleanup-tmp-roles.sh b/scripts/cleanup-tmp-roles.sh new file mode 100644 index 0000000..8f06421 --- /dev/null +++ b/scripts/cleanup-tmp-roles.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Cleans up custom roles and role assignments created by MPF (names prefixed tmp-rol-). +# Usage: SUBSCRIPTION_ID= ./cleanup-tmp-roles.sh +# Optional: TMP_ROLE_PREFIX (default: tmp-rol-) + +if ! command -v az >/dev/null 2>&1; then + echo "Azure CLI (az) is required" >&2 + exit 1 +fi +if ! command -v jq >/dev/null 2>&1; then + echo "jq is required" >&2 + exit 1 +fi + +SUBSCRIPTION_ID="${SUBSCRIPTION_ID:-${1:-}}" +ROLE_PREFIX="${TMP_ROLE_PREFIX:-tmp-rol-}" + +if [[ -z "${SUBSCRIPTION_ID}" ]]; then + SUBSCRIPTION_ID=$(az account show --query id -o tsv 2>/dev/null || true) +fi + +if [[ -z "${SUBSCRIPTION_ID}" ]]; then + echo "Subscription not provided and no default found. Set SUBSCRIPTION_ID or login." >&2 + exit 1 +fi + +az account set --subscription "${SUBSCRIPTION_ID}" + +query="[?starts_with(roleName, '$ROLE_PREFIX')].[id,name,roleName]" +defs_json=$(az role definition list --custom-role-only true --subscription "${SUBSCRIPTION_ID}" \ + --query "${query}" -o json) + +if [[ "$(echo "${defs_json}" | jq 'length')" -eq 0 ]]; then + echo "No tmp-rol* custom roles found" + exit 0 +fi + +echo "${defs_json}" | jq -c '.[]' | while read -r def; do + role_id=$(echo "${def}" | jq -r '.[0]') + role_name=$(echo "${def}" | jq -r '.[1]') + role_display=$(echo "${def}" | jq -r '.[2]') + + echo "Processing role ${role_display} (${role_name})" + + assignments=$(az role assignment list --all --subscription "${SUBSCRIPTION_ID}" --role "${role_id}" --query '[].id' -o tsv) + if [[ -n "${assignments}" ]]; then + echo "${assignments}" | while read -r assign_id; do + [[ -z "${assign_id}" ]] && continue + echo "Deleting assignment ${assign_id}" + az role assignment delete --ids "${assign_id}" + done + else + echo "No assignments for ${role_display}" + fi + + echo "Deleting role definition ${role_name}" + az role definition delete --name "${role_name}" --subscription "${SUBSCRIPTION_ID}" + echo "Done ${role_display}" + echo +done diff --git a/scripts/New-E2EServicePrincipals.ps1 b/scripts/create-e2e-service-principals.ps1 similarity index 100% rename from scripts/New-E2EServicePrincipals.ps1 rename to scripts/create-e2e-service-principals.ps1 diff --git a/scripts/create-e2e-service-principals.sh b/scripts/create-e2e-service-principals.sh index 49f1dcc..1737561 100755 --- a/scripts/create-e2e-service-principals.sh +++ b/scripts/create-e2e-service-principals.sh @@ -59,7 +59,7 @@ EOF parse_args() { while [[ $# -gt 0 ]]; do case "$1" in - -y|--yes) + -y | --yes) YES=true shift ;; @@ -67,11 +67,11 @@ parse_args() { ADD_TO_GITHUB=true shift ;; - -o|--output-file) + -o | --output-file) OUTPUT_FILE="$2" shift 2 ;; - -h|--help) + -h | --help) usage exit 0 ;;