From 4faaddad5900101ca3a39cd4cec97cad49678317 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 May 2026 14:31:13 +0000 Subject: [PATCH 01/15] Initial plan From ee3d6a20b3eabdc6dbd1ab6be7b985242878d1ba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 May 2026 14:42:43 +0000 Subject: [PATCH 02/15] feat: add end-to-end test workflows for all devops-infra actions Agent-Logs-Url: https://github.com/devops-infra/end-to-end-tests/sessions/b430f53f-9c34-435e-b906-a500c6daf9ed Co-authored-by: ChristophShyper <45788587+ChristophShyper@users.noreply.github.com> --- .../workflows/auto-pull-request-create.yml | 1 + .github/workflows/cron-e2e-tests.yml | 40 +++++ .github/workflows/e2e-action-commit-push.yml | 139 +++++++++++++++ .../e2e-action-container-structure-test.yml | 145 ++++++++++++++++ .github/workflows/e2e-action-format-hcl.yml | 135 +++++++++++++++ .github/workflows/e2e-action-pull-request.yml | 160 +++++++++++++++++ .../e2e-action-terraform-copy-vars.yml | 161 ++++++++++++++++++ .../e2e-action-terraform-validate.yml | 84 +++++++++ .github/workflows/e2e-action-tflint.yml | 112 ++++++++++++ .../alpine-extended.yml | 12 ++ .../container-structure-test/alpine.yml | 21 +++ 11 files changed, 1010 insertions(+) create mode 100644 .github/workflows/cron-e2e-tests.yml create mode 100644 .github/workflows/e2e-action-commit-push.yml create mode 100644 .github/workflows/e2e-action-container-structure-test.yml create mode 100644 .github/workflows/e2e-action-format-hcl.yml create mode 100644 .github/workflows/e2e-action-pull-request.yml create mode 100644 .github/workflows/e2e-action-terraform-copy-vars.yml create mode 100644 .github/workflows/e2e-action-terraform-validate.yml create mode 100644 .github/workflows/e2e-action-tflint.yml create mode 100644 tests/fixtures/container-structure-test/alpine-extended.yml create mode 100644 tests/fixtures/container-structure-test/alpine.yml diff --git a/.github/workflows/auto-pull-request-create.yml b/.github/workflows/auto-pull-request-create.yml index 4e9c738..fc7ef7c 100644 --- a/.github/workflows/auto-pull-request-create.yml +++ b/.github/workflows/auto-pull-request-create.yml @@ -7,6 +7,7 @@ on: - main - release/** - dependabot/** + - test/** permissions: contents: read diff --git a/.github/workflows/cron-e2e-tests.yml b/.github/workflows/cron-e2e-tests.yml new file mode 100644 index 0000000..c1ea2a4 --- /dev/null +++ b/.github/workflows/cron-e2e-tests.yml @@ -0,0 +1,40 @@ +name: (Cron) End-to-End Tests + +on: + schedule: + - cron: 0 6 * * 1 + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + issues: write + +jobs: + action-commit-push: + uses: ./.github/workflows/e2e-action-commit-push.yml + secrets: inherit + + action-pull-request: + uses: ./.github/workflows/e2e-action-pull-request.yml + secrets: inherit + + action-tflint: + uses: ./.github/workflows/e2e-action-tflint.yml + secrets: inherit + + action-format-hcl: + uses: ./.github/workflows/e2e-action-format-hcl.yml + secrets: inherit + + action-terraform-validate: + uses: ./.github/workflows/e2e-action-terraform-validate.yml + secrets: inherit + + action-terraform-copy-vars: + uses: ./.github/workflows/e2e-action-terraform-copy-vars.yml + secrets: inherit + + action-container-structure-test: + uses: ./.github/workflows/e2e-action-container-structure-test.yml + secrets: inherit diff --git a/.github/workflows/e2e-action-commit-push.yml b/.github/workflows/e2e-action-commit-push.yml new file mode 100644 index 0000000..d1a2e02 --- /dev/null +++ b/.github/workflows/e2e-action-commit-push.yml @@ -0,0 +1,139 @@ +name: (E2E) Action Commit Push + +on: + workflow_call: + workflow_dispatch: + +permissions: + contents: write + +jobs: + basic-commit: + name: Basic commit and push to new branch + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Create test file + run: echo "E2E basic commit test $(date -u)" > e2e-test-file.txt + + - name: Commit and push to new branch + id: commit + uses: devops-infra/action-commit-push@v1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + commit_message: "[E2E] Basic commit test" + target_branch: test/e2e-commit-push-basic-${{ github.run_id }} + + - name: Verify outputs + run: | + echo "branch_name=${{ steps.commit.outputs.branch_name }}" + echo "files_changed=${{ steps.commit.outputs.files_changed }}" + test -n "${{ steps.commit.outputs.branch_name }}" + test -n "${{ steps.commit.outputs.files_changed }}" + + - name: Cleanup - delete test branch + if: always() + run: git push origin --delete test/e2e-commit-push-basic-${{ github.run_id }} 2>/dev/null || true + + commit-with-prefix-message: + name: Commit with custom prefix and message + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Create test file + run: echo "E2E prefix message test $(date -u)" > e2e-prefix-test.txt + + - name: Commit with custom prefix and message + id: commit + uses: devops-infra/action-commit-push@v1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + commit_prefix: "[E2E-PREFIX]" + commit_message: " Custom message test" + target_branch: test/e2e-commit-push-prefix-${{ github.run_id }} + + - name: Verify outputs + run: | + echo "branch_name=${{ steps.commit.outputs.branch_name }}" + echo "files_changed=${{ steps.commit.outputs.files_changed }}" + test -n "${{ steps.commit.outputs.branch_name }}" + test -n "${{ steps.commit.outputs.files_changed }}" + + - name: Cleanup - delete test branch + if: always() + run: git push origin --delete test/e2e-commit-push-prefix-${{ github.run_id }} 2>/dev/null || true + + allow-empty-commit: + name: Allow empty commit with no file changes + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Commit with no file changes to new branch + id: commit + uses: devops-infra/action-commit-push@v1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + allow_empty_commit: "true" + commit_message: "[E2E] Empty commit test" + target_branch: test/e2e-commit-push-empty-${{ github.run_id }} + + - name: Verify branch output + run: | + echo "branch_name=${{ steps.commit.outputs.branch_name }}" + test -n "${{ steps.commit.outputs.branch_name }}" + + - name: Cleanup - delete test branch + if: always() + run: git push origin --delete test/e2e-commit-push-empty-${{ github.run_id }} 2>/dev/null || true + + amend-commit: + name: Amend previous commit with force-with-lease + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Create initial branch with commit + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -b test/e2e-commit-push-amend-${{ github.run_id }} + echo "v1" > e2e-amend-test.txt + git add e2e-amend-test.txt + git commit -m "E2E: initial commit for amend test" + git push origin test/e2e-commit-push-amend-${{ github.run_id }} + + - name: Add more content to file + run: echo "v2" >> e2e-amend-test.txt + + - name: Amend commit keeping original message + id: amend + uses: devops-infra/action-commit-push@v1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + amend: "true" + no_edit: "true" + force_with_lease: "true" + + - name: Verify branch output + run: | + echo "branch_name=${{ steps.amend.outputs.branch_name }}" + test -n "${{ steps.amend.outputs.branch_name }}" + + - name: Cleanup - delete test branch + if: always() + run: git push origin --delete test/e2e-commit-push-amend-${{ github.run_id }} 2>/dev/null || true diff --git a/.github/workflows/e2e-action-container-structure-test.yml b/.github/workflows/e2e-action-container-structure-test.yml new file mode 100644 index 0000000..c43d15e --- /dev/null +++ b/.github/workflows/e2e-action-container-structure-test.yml @@ -0,0 +1,145 @@ +name: (E2E) Action Container Structure Test + +on: + workflow_call: + workflow_dispatch: + +permissions: + contents: read + +jobs: + text-output: + name: Basic test with text output format + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Pull Alpine test image + run: docker pull alpine:latest + + - name: Run container structure tests with text output + id: cst + uses: devops-infra/action-container-structure-test@v1 + with: + image: alpine:latest + config: tests/fixtures/container-structure-test/alpine.yml + output: text + + - name: Verify test result outputs + run: | + echo "total=${{ steps.cst.outputs.total }}" + echo "passed=${{ steps.cst.outputs.passed }}" + echo "failed=${{ steps.cst.outputs.failed }}" + echo "exit_code=${{ steps.cst.outputs.exit_code }}" + test "${{ steps.cst.outputs.exit_code }}" = "0" + test "${{ steps.cst.outputs.failed }}" = "0" + + json-output: + name: Test with JSON output format + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Pull Alpine test image + run: docker pull alpine:latest + + - name: Run container structure tests with JSON output + id: cst + uses: devops-infra/action-container-structure-test@v1 + with: + image: alpine:latest + config: tests/fixtures/container-structure-test/alpine.yml + output: json + + - name: Verify test result outputs + run: | + echo "total=${{ steps.cst.outputs.total }}" + echo "passed=${{ steps.cst.outputs.passed }}" + echo "exit_code=${{ steps.cst.outputs.exit_code }}" + test "${{ steps.cst.outputs.exit_code }}" = "0" + + junit-output: + name: Test with JUnit output format and suite name + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Pull Alpine test image + run: docker pull alpine:latest + + - name: Run container structure tests with JUnit output + id: cst + uses: devops-infra/action-container-structure-test@v1 + with: + image: alpine:latest + config: tests/fixtures/container-structure-test/alpine.yml + output: junit + junit_suite_name: e2e-alpine-tests + + - name: Verify test result outputs + run: | + echo "total=${{ steps.cst.outputs.total }}" + echo "passed=${{ steps.cst.outputs.passed }}" + echo "exit_code=${{ steps.cst.outputs.exit_code }}" + test "${{ steps.cst.outputs.exit_code }}" = "0" + + test-report-file: + name: Test with report written to file + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Pull Alpine test image + run: docker pull alpine:latest + + - name: Run container structure tests saving report to file + id: cst + uses: devops-infra/action-container-structure-test@v1 + with: + image: alpine:latest + config: tests/fixtures/container-structure-test/alpine.yml + output: json + test_report: /tmp/cst-report.json + + - name: Verify report file was created + run: test -f /tmp/cst-report.json + + - name: Verify test result outputs + run: | + echo "total=${{ steps.cst.outputs.total }}" + echo "passed=${{ steps.cst.outputs.passed }}" + echo "exit_code=${{ steps.cst.outputs.exit_code }}" + test "${{ steps.cst.outputs.exit_code }}" = "0" + + multiple-config-files: + name: Test with multiple config files + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Pull Alpine test image + run: docker pull alpine:latest + + - name: Run container structure tests with multiple config files + id: cst + uses: devops-infra/action-container-structure-test@v1 + with: + image: alpine:latest + config: | + tests/fixtures/container-structure-test/alpine.yml + tests/fixtures/container-structure-test/alpine-extended.yml + output: text + + - name: Verify combined test results + run: | + echo "total=${{ steps.cst.outputs.total }}" + echo "passed=${{ steps.cst.outputs.passed }}" + echo "failed=${{ steps.cst.outputs.failed }}" + echo "exit_code=${{ steps.cst.outputs.exit_code }}" + test "${{ steps.cst.outputs.exit_code }}" = "0" + test "${{ steps.cst.outputs.failed }}" = "0" diff --git a/.github/workflows/e2e-action-format-hcl.yml b/.github/workflows/e2e-action-format-hcl.yml new file mode 100644 index 0000000..c1b4d6a --- /dev/null +++ b/.github/workflows/e2e-action-format-hcl.yml @@ -0,0 +1,135 @@ +name: (E2E) Action Format HCL + +on: + workflow_call: + workflow_dispatch: + +permissions: + contents: read + +jobs: + format-check-clean-files: + name: Check mode on already-formatted files passes + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Create well-formatted Terraform files + run: | + mkdir -p tests/e2e-hcl-clean + cat > tests/e2e-hcl-clean/main.tf << 'EOF' + variable "name" { + description = "Resource name" + type = string + } + + variable "region" { + description = "Cloud region" + type = string + default = "us-east-1" + } + + locals { + label = "${var.name}-${var.region}" + } + + output "label" { + value = local.label + } + EOF + + - name: Check already-formatted files + uses: devops-infra/action-format-hcl@v1 + with: + check: "true" + dir: tests/e2e-hcl-clean + + format-write-mode: + name: Write mode formats and rewrites files + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Create Terraform files for write-mode formatting + run: | + mkdir -p tests/e2e-hcl-write + cat > tests/e2e-hcl-write/main.tf << 'EOF' + variable "name" { + description = "Resource name" + type = string + } + + locals { + upper_name = upper(var.name) + } + + output "result" { + value = local.upper_name + } + EOF + + - name: Run format in write mode + id: format + uses: devops-infra/action-format-hcl@v1 + with: + write: "true" + diff: "true" + dir: tests/e2e-hcl-write + + - name: Show files changed output + run: echo "files_changed=${{ steps.format.outputs.files_changed }}" + + format-check-malformed-files: + name: Check mode on malformed files reports failure + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Create malformed Terraform file + run: | + mkdir -p tests/e2e-hcl-malformed + printf 'variable "foo" {\ntype = string\ndescription="bad spacing"\n}\n' > tests/e2e-hcl-malformed/malformed.tf + + - name: Check malformed files - expected to fail + id: check + uses: devops-infra/action-format-hcl@v1 + continue-on-error: true + with: + check: "true" + dir: tests/e2e-hcl-malformed + + - name: Verify action detected formatting issues + run: test "${{ steps.check.outcome }}" = "failure" + + format-list-with-diff: + name: List and diff mode on formatted files + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Create Terraform files for list/diff mode + run: | + mkdir -p tests/e2e-hcl-list + cat > tests/e2e-hcl-list/variables.tf << 'EOF' + variable "env" { + description = "Deployment environment" + type = string + default = "dev" + } + EOF + + - name: Run format in list and diff mode + id: format + uses: devops-infra/action-format-hcl@v1 + with: + list: "true" + diff: "true" + write: "false" + dir: tests/e2e-hcl-list + + - name: Show files changed output + run: echo "files_changed=${{ steps.format.outputs.files_changed }}" diff --git a/.github/workflows/e2e-action-pull-request.yml b/.github/workflows/e2e-action-pull-request.yml new file mode 100644 index 0000000..c0c959b --- /dev/null +++ b/.github/workflows/e2e-action-pull-request.yml @@ -0,0 +1,160 @@ +name: (E2E) Action Pull Request + +on: + workflow_call: + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + issues: write + +jobs: + basic-pull-request: + name: Basic pull request creation + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Create test branch with a commit + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -b test/e2e-pr-basic-${{ github.run_id }} + echo "E2E PR basic test $(date -u)" > e2e-pr-test.txt + git add e2e-pr-test.txt + git commit -m "[E2E] Basic PR test" + git push origin test/e2e-pr-basic-${{ github.run_id }} + + - name: Create pull request + id: pr + uses: devops-infra/action-pull-request@v1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + title: "[E2E] Basic pull request test - safe to close" + body: "Automated end-to-end test pull request. Will be closed automatically." + target_branch: master + + - name: Verify outputs + run: | + echo "url=${{ steps.pr.outputs.url }}" + echo "pr_number=${{ steps.pr.outputs.pr_number }}" + test -n "${{ steps.pr.outputs.url }}" + test -n "${{ steps.pr.outputs.pr_number }}" + + - name: Cleanup - close PR and delete test branch + if: always() + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + PR_NUMBER="${{ steps.pr.outputs.pr_number }}" + if [ -n "$PR_NUMBER" ]; then + gh pr close "$PR_NUMBER" --repo "${{ github.repository }}" 2>/dev/null || true + fi + git push origin --delete test/e2e-pr-basic-${{ github.run_id }} 2>/dev/null || true + + pull-request-with-title-body: + name: Pull request with custom title and body + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Create test branch with a commit + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -b test/e2e-pr-custom-${{ github.run_id }} + echo "E2E PR custom title/body test $(date -u)" > e2e-pr-custom-test.txt + git add e2e-pr-custom-test.txt + git commit -m "[E2E] Custom title/body PR test" + git push origin test/e2e-pr-custom-${{ github.run_id }} + + - name: Create pull request with custom title and body + id: pr + uses: devops-infra/action-pull-request@v1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + title: "[E2E] Custom title test - safe to close" + body: | + Automated end-to-end test. + Testing custom title and body fields. + Safe to close automatically. + target_branch: master + allow_no_diff: "false" + + - name: Verify outputs + run: | + echo "url=${{ steps.pr.outputs.url }}" + echo "pr_number=${{ steps.pr.outputs.pr_number }}" + test -n "${{ steps.pr.outputs.url }}" + test -n "${{ steps.pr.outputs.pr_number }}" + + - name: Cleanup - close PR and delete test branch + if: always() + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + PR_NUMBER="${{ steps.pr.outputs.pr_number }}" + if [ -n "$PR_NUMBER" ]; then + gh pr close "$PR_NUMBER" --repo "${{ github.repository }}" 2>/dev/null || true + fi + git push origin --delete test/e2e-pr-custom-${{ github.run_id }} 2>/dev/null || true + + pull-request-draft-with-diff: + name: Draft pull request with get_diff enabled + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Create test branch with a commit + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -b test/e2e-pr-draft-${{ github.run_id }} + echo "E2E draft PR test $(date -u)" > e2e-pr-draft-test.txt + git add e2e-pr-draft-test.txt + git commit -m "[E2E] Draft PR with diff test" + git push origin test/e2e-pr-draft-${{ github.run_id }} + + - name: Create draft pull request with diff injection + id: pr + uses: devops-infra/action-pull-request@v1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + title: "[E2E] Draft PR with diff - safe to close" + body: | + Automated end-to-end test. + + + + + target_branch: master + draft: "true" + get_diff: "true" + + - name: Verify outputs + run: | + echo "url=${{ steps.pr.outputs.url }}" + echo "pr_number=${{ steps.pr.outputs.pr_number }}" + test -n "${{ steps.pr.outputs.url }}" + test -n "${{ steps.pr.outputs.pr_number }}" + + - name: Cleanup - close PR and delete test branch + if: always() + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + PR_NUMBER="${{ steps.pr.outputs.pr_number }}" + if [ -n "$PR_NUMBER" ]; then + gh pr close "$PR_NUMBER" --repo "${{ github.repository }}" 2>/dev/null || true + fi + git push origin --delete test/e2e-pr-draft-${{ github.run_id }} 2>/dev/null || true diff --git a/.github/workflows/e2e-action-terraform-copy-vars.yml b/.github/workflows/e2e-action-terraform-copy-vars.yml new file mode 100644 index 0000000..ad2e31f --- /dev/null +++ b/.github/workflows/e2e-action-terraform-copy-vars.yml @@ -0,0 +1,161 @@ +name: (E2E) Action Terraform Copy Vars + +on: + workflow_call: + workflow_dispatch: + +permissions: + contents: read + +jobs: + basic-copy-vars: + name: Copy variables from central file to modules + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Create Terraform module structure with all-variables file + run: | + mkdir -p terraform/module_a terraform/module_b + + cat > all-variables.tf << 'EOF' + variable "region" { + description = "Cloud region for deployment" + type = string + default = "us-east-1" + } + + variable "name" { + description = "Resource name" + type = string + } + + variable "env" { + description = "Deployment environment" + type = string + default = "dev" + } + EOF + + cat > terraform/module_a/variables.tf << 'EOF' + variable "region" { + description = "Cloud region for deployment" + type = string + default = "us-east-1" + } + EOF + + cat > terraform/module_b/variables.tf << 'EOF' + variable "name" { + description = "Resource name" + type = string + } + EOF + + - name: Copy variables from central file to all modules + id: copy + uses: devops-infra/action-terraform-copy-vars@v1 + with: + dirs_with_modules: terraform + files_with_vars: variables.tf + all_vars_file: all-variables.tf + fail_on_missing: "false" + + - name: Show files changed output + run: echo "files_changed=${{ steps.copy.outputs.files_changed }}" + + copy-vars-custom-paths: + name: Copy variables with custom directory and file paths + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Create custom Terraform structure + run: | + mkdir -p infra/services/api infra/services/db + + cat > infra/all-vars.tf << 'EOF' + variable "app_name" { + description = "Application name" + type = string + } + + variable "version_tag" { + description = "Docker image tag" + type = string + default = "latest" + } + EOF + + cat > infra/services/api/vars.tf << 'EOF' + variable "app_name" { + description = "Application name" + type = string + } + EOF + + cat > infra/services/db/vars.tf << 'EOF' + variable "app_name" { + description = "Application name" + type = string + } + EOF + + - name: Copy variables using custom paths + id: copy + uses: devops-infra/action-terraform-copy-vars@v1 + with: + dirs_with_modules: infra/services + files_with_vars: vars.tf + all_vars_file: infra/all-vars.tf + fail_on_missing: "false" + + - name: Show files changed output + run: echo "files_changed=${{ steps.copy.outputs.files_changed }}" + + copy-vars-fail-on-missing: + name: Fail when module uses variable missing from central file + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Create Terraform structure with missing variable + run: | + mkdir -p terraform/module_missing + + cat > all-variables.tf << 'EOF' + variable "region" { + description = "Cloud region" + type = string + default = "us-east-1" + } + EOF + + cat > terraform/module_missing/variables.tf << 'EOF' + variable "region" { + description = "Cloud region" + type = string + default = "us-east-1" + } + + variable "missing_var" { + description = "This variable is not defined in all-variables.tf" + type = string + } + EOF + + - name: Run copy-vars with fail_on_missing - expected to fail + id: copy + uses: devops-infra/action-terraform-copy-vars@v1 + continue-on-error: true + with: + dirs_with_modules: terraform + files_with_vars: variables.tf + all_vars_file: all-variables.tf + fail_on_missing: "true" + + - name: Verify action failed on missing variable + run: test "${{ steps.copy.outcome }}" = "failure" diff --git a/.github/workflows/e2e-action-terraform-validate.yml b/.github/workflows/e2e-action-terraform-validate.yml new file mode 100644 index 0000000..5cc6267 --- /dev/null +++ b/.github/workflows/e2e-action-terraform-validate.yml @@ -0,0 +1,84 @@ +name: (E2E) Action Terraform Validate + +on: + workflow_call: + workflow_dispatch: + +permissions: + contents: read + +jobs: + validate-basic: + name: Validate valid Terraform configuration + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Create valid Terraform module + run: | + mkdir -p terraform/module_validate + cat > terraform/module_validate/main.tf << 'EOF' + variable "name" { + description = "Resource name" + type = string + } + + variable "env" { + description = "Deployment environment" + type = string + default = "dev" + } + + locals { + full_name = "${var.name}-${var.env}" + } + + output "full_name" { + value = local.full_name + } + EOF + + - name: Validate all Terraform modules + uses: devops-infra/action-terraform-validate@v1 + with: + dir_filter: terraform + + validate-with-dir-filter: + name: Validate with explicit directory filter + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Create multiple Terraform modules + run: | + mkdir -p terraform/module_a terraform/module_b + + cat > terraform/module_a/main.tf << 'EOF' + variable "name" { + description = "Name of the resource" + type = string + } + + output "name_upper" { + value = upper(var.name) + } + EOF + + cat > terraform/module_b/main.tf << 'EOF' + variable "count_value" { + description = "Number of instances" + type = number + default = 1 + } + + output "count_doubled" { + value = var.count_value * 2 + } + EOF + + - name: Validate only module_a + uses: devops-infra/action-terraform-validate@v1 + with: + dir_filter: terraform/module_a diff --git a/.github/workflows/e2e-action-tflint.yml b/.github/workflows/e2e-action-tflint.yml new file mode 100644 index 0000000..92ba722 --- /dev/null +++ b/.github/workflows/e2e-action-tflint.yml @@ -0,0 +1,112 @@ +name: (E2E) Action TFLint + +on: + workflow_call: + workflow_dispatch: + +permissions: + contents: read + +jobs: + basic-tflint: + name: Basic TFLint on valid Terraform files + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Create valid Terraform module + run: | + mkdir -p terraform/module_basic + cat > terraform/module_basic/main.tf << 'EOF' + variable "name" { + description = "Resource name" + type = string + } + + locals { + upper_name = upper(var.name) + } + + output "upper_name" { + value = local.upper_name + } + EOF + + - name: Run TFLint on all Terraform directories + uses: devops-infra/action-tflint@v1 + with: + dir_filter: terraform + run_init: "false" + fail_on_changes: "false" + + tflint-with-dir-filter: + name: TFLint with specific directory filter + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Create multiple Terraform modules + run: | + mkdir -p terraform/module_aws terraform/module_gcp + + cat > terraform/module_aws/main.tf << 'EOF' + variable "region" { + description = "AWS region" + type = string + default = "us-east-1" + } + + output "region" { + value = var.region + } + EOF + + cat > terraform/module_gcp/main.tf << 'EOF' + variable "project" { + description = "GCP project ID" + type = string + } + + output "project" { + value = var.project + } + EOF + + - name: Run TFLint only on aws module + uses: devops-infra/action-tflint@v1 + with: + dir_filter: terraform/module_aws + run_init: "false" + fail_on_changes: "false" + + tflint-no-fail-on-changes: + name: TFLint with fail_on_changes disabled + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Create Terraform module with lint warning + run: | + mkdir -p terraform/module_warn + + cat > terraform/module_warn/main.tf << 'EOF' + variable "unused_var" { + description = "This variable is intentionally unused to trigger a warning" + type = string + default = "unused" + } + + output "constant" { + value = "hello" + } + EOF + + - name: Run TFLint without failing on findings + uses: devops-infra/action-tflint@v1 + with: + dir_filter: terraform/module_warn + run_init: "false" + fail_on_changes: "false" diff --git a/tests/fixtures/container-structure-test/alpine-extended.yml b/tests/fixtures/container-structure-test/alpine-extended.yml new file mode 100644 index 0000000..6976042 --- /dev/null +++ b/tests/fixtures/container-structure-test/alpine-extended.yml @@ -0,0 +1,12 @@ +schemaVersion: '2.0.0' + +commandTests: + - name: Alpine package manager available + command: apk + args: [--version] + expectedOutput: [apk-tools] + +fileExistenceTests: + - name: /usr/bin/env exists + path: /usr/bin/env + shouldExist: true diff --git a/tests/fixtures/container-structure-test/alpine.yml b/tests/fixtures/container-structure-test/alpine.yml new file mode 100644 index 0000000..e95526e --- /dev/null +++ b/tests/fixtures/container-structure-test/alpine.yml @@ -0,0 +1,21 @@ +schemaVersion: '2.0.0' + +commandTests: + - name: Alpine OS identification + command: cat + args: [/etc/os-release] + expectedOutput: [ID=alpine] + + - name: Shell available + command: /bin/sh + args: [-c, echo ok] + expectedOutput: [ok] + +fileExistenceTests: + - name: /etc/os-release exists + path: /etc/os-release + shouldExist: true + + - name: /bin/sh exists + path: /bin/sh + shouldExist: true From 2aa59778016757adce79bbcfbbbcdfdf25442c03 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 May 2026 15:12:12 +0000 Subject: [PATCH 03/15] test: add repository and repository_path coverage in action-pull-request e2e workflow Agent-Logs-Url: https://github.com/devops-infra/end-to-end-tests/sessions/dd39da71-3100-4bc0-90b0-bda2c04dfa4f Co-authored-by: ChristophShyper <45788587+ChristophShyper@users.noreply.github.com> --- .github/workflows/e2e-action-pull-request.yml | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/.github/workflows/e2e-action-pull-request.yml b/.github/workflows/e2e-action-pull-request.yml index c0c959b..200bea9 100644 --- a/.github/workflows/e2e-action-pull-request.yml +++ b/.github/workflows/e2e-action-pull-request.yml @@ -106,6 +106,57 @@ jobs: fi git push origin --delete test/e2e-pr-custom-${{ github.run_id }} 2>/dev/null || true + pull-request-with-repository-path: + name: Pull request using custom checkout path and explicit repository + runs-on: ubuntu-latest + steps: + - name: Checkout repository into custom path + uses: actions/checkout@v4 + with: + fetch-depth: 0 + path: work/repo + + - name: Create test branch with a commit inside custom path + run: | + cd work/repo + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -b test/e2e-pr-repo-path-${{ github.run_id }} + echo "E2E PR repository_path test $(date -u)" > e2e-pr-path-test.txt + git add e2e-pr-path-test.txt + git commit -m "[E2E] PR repository_path test" + git push origin test/e2e-pr-repo-path-${{ github.run_id }} + + - name: Create pull request with repository and repository_path + id: pr + uses: devops-infra/action-pull-request@v1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + repository: ${{ github.repository }} + repository_path: work/repo + title: "[E2E] Repository path PR test - safe to close" + body: "Automated end-to-end test for repository_path input." + target_branch: master + + - name: Verify outputs + run: | + echo "url=${{ steps.pr.outputs.url }}" + echo "pr_number=${{ steps.pr.outputs.pr_number }}" + test -n "${{ steps.pr.outputs.url }}" + test -n "${{ steps.pr.outputs.pr_number }}" + + - name: Cleanup - close PR and delete test branch + if: always() + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + PR_NUMBER="${{ steps.pr.outputs.pr_number }}" + if [ -n "$PR_NUMBER" ]; then + gh pr close "$PR_NUMBER" --repo "${{ github.repository }}" 2>/dev/null || true + fi + cd work/repo + git push origin --delete test/e2e-pr-repo-path-${{ github.run_id }} 2>/dev/null || true + pull-request-draft-with-diff: name: Draft pull request with get_diff enabled runs-on: ubuntu-latest From c23b94bf7b09e32d5b161f7cf0e8074b64d62532 Mon Sep 17 00:00:00 2001 From: ChristophShyper <45788587+ChristophShyper@users.noreply.github.com> Date: Sun, 24 May 2026 13:43:52 +0200 Subject: [PATCH 04/15] feat: enhance end-to-end testing framework with new workflows and coverage reporting --- .env.example | 8 +- .github/workflows/cron-e2e-tests.yml | 47 +++ .github/workflows/e2e-action-commit-push.yml | 293 +++++++++++++++++- .../e2e-action-container-structure-test.yml | 132 +++++++- .github/workflows/e2e-action-format-hcl.yml | 114 ++++++- .github/workflows/e2e-action-pull-request.yml | 68 +++- .../workflows/e2e-action-template-action.yml | 81 +++++ .../e2e-action-terraform-copy-vars.yml | 97 +++++- .../e2e-action-terraform-validate.yml | 54 +++- .github/workflows/e2e-action-tflint.yml | 190 +++++++++++- .gitignore | 10 +- .pre-commit-config.yaml | 5 + .pylintrc | 1 + README.md | 143 +++++++-- Taskfile.cicd.yml | 41 +++ Taskfile.scripts.yml | 113 +++++++ Taskfile.variables.yml | 7 + scripts/check_action_input_coverage.py | 224 +++++++++++++ tests/coverage-baseline.json | 9 + triglav.jpeg | Bin 0 -> 145328 bytes 20 files changed, 1577 insertions(+), 60 deletions(-) create mode 100644 .github/workflows/e2e-action-template-action.yml create mode 100644 .pylintrc create mode 100644 scripts/check_action_input_coverage.py create mode 100644 tests/coverage-baseline.json create mode 100644 triglav.jpeg diff --git a/.env.example b/.env.example index f91305b..e72fdfe 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,2 @@ -DOCKER_USERNAME=your-dockerhub-user -DOCKER_ORG_NAME=your-dockerhub-org -DOCKER_TOKEN=your-docker-token -GITHUB_USERNAME=your-github-user -GITHUB_ORG_NAME=your-github-org -GITHUB_TOKEN=your-github-token +GITHUB_TOKEN=ghp_your_token_here +GH_REPO=devops-infra/end-to-end-tests diff --git a/.github/workflows/cron-e2e-tests.yml b/.github/workflows/cron-e2e-tests.yml index c1ea2a4..281d329 100644 --- a/.github/workflows/cron-e2e-tests.yml +++ b/.github/workflows/cron-e2e-tests.yml @@ -4,6 +4,25 @@ on: schedule: - cron: 0 6 * * 1 workflow_dispatch: + inputs: + mode: + description: Run mode (ref or image) + required: false + default: ref + type: choice + options: + - ref + - image + action_ref: + description: Action git ref used when mode=ref + required: false + default: v1 + type: string + image_tag: + description: Action image tag used when mode=image (for example v1.2.3-test or v1.2.3-rc) + required: false + default: '' + type: string permissions: contents: write @@ -13,28 +32,56 @@ permissions: jobs: action-commit-push: uses: ./.github/workflows/e2e-action-commit-push.yml + with: + mode: ${{ github.event_name == 'workflow_dispatch' && inputs.mode || 'ref' }} + action_ref: ${{ github.event_name == 'workflow_dispatch' && inputs.action_ref || 'v1' }} + image_tag: ${{ github.event_name == 'workflow_dispatch' && inputs.image_tag || '' }} secrets: inherit action-pull-request: uses: ./.github/workflows/e2e-action-pull-request.yml + with: + mode: ${{ github.event_name == 'workflow_dispatch' && inputs.mode || 'ref' }} + action_ref: ${{ github.event_name == 'workflow_dispatch' && inputs.action_ref || 'v1' }} + image_tag: ${{ github.event_name == 'workflow_dispatch' && inputs.image_tag || '' }} secrets: inherit action-tflint: uses: ./.github/workflows/e2e-action-tflint.yml + with: + mode: ${{ github.event_name == 'workflow_dispatch' && inputs.mode || 'ref' }} + action_ref: ${{ github.event_name == 'workflow_dispatch' && inputs.action_ref || 'v1' }} + image_tag: ${{ github.event_name == 'workflow_dispatch' && inputs.image_tag || '' }} secrets: inherit action-format-hcl: uses: ./.github/workflows/e2e-action-format-hcl.yml + with: + mode: ${{ github.event_name == 'workflow_dispatch' && inputs.mode || 'ref' }} + action_ref: ${{ github.event_name == 'workflow_dispatch' && inputs.action_ref || 'v1' }} + image_tag: ${{ github.event_name == 'workflow_dispatch' && inputs.image_tag || '' }} secrets: inherit action-terraform-validate: uses: ./.github/workflows/e2e-action-terraform-validate.yml + with: + mode: ${{ github.event_name == 'workflow_dispatch' && inputs.mode || 'ref' }} + action_ref: ${{ github.event_name == 'workflow_dispatch' && inputs.action_ref || 'v1' }} + image_tag: ${{ github.event_name == 'workflow_dispatch' && inputs.image_tag || '' }} secrets: inherit action-terraform-copy-vars: uses: ./.github/workflows/e2e-action-terraform-copy-vars.yml + with: + mode: ${{ github.event_name == 'workflow_dispatch' && inputs.mode || 'ref' }} + action_ref: ${{ github.event_name == 'workflow_dispatch' && inputs.action_ref || 'v1' }} + image_tag: ${{ github.event_name == 'workflow_dispatch' && inputs.image_tag || '' }} secrets: inherit action-container-structure-test: uses: ./.github/workflows/e2e-action-container-structure-test.yml + with: + mode: ${{ github.event_name == 'workflow_dispatch' && inputs.mode || 'ref' }} + action_ref: ${{ github.event_name == 'workflow_dispatch' && inputs.action_ref || 'v1' }} + image_tag: ${{ github.event_name == 'workflow_dispatch' && inputs.image_tag || '' }} secrets: inherit diff --git a/.github/workflows/e2e-action-commit-push.yml b/.github/workflows/e2e-action-commit-push.yml index d1a2e02..40d2e1e 100644 --- a/.github/workflows/e2e-action-commit-push.yml +++ b/.github/workflows/e2e-action-commit-push.yml @@ -2,6 +2,22 @@ name: (E2E) Action Commit Push on: workflow_call: + inputs: + action_ref: + description: Git ref of devops-infra/action-commit-push to validate + required: false + type: string + default: v1 + mode: + description: Execution mode (ref or image) + required: false + type: string + default: ref + image_tag: + description: Image tag for docker mode (for example v1.2.3-test or v1.2.3-rc) + required: false + type: string + default: '' workflow_dispatch: permissions: @@ -13,7 +29,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 @@ -21,6 +37,7 @@ jobs: run: echo "E2E basic commit test $(date -u)" > e2e-test-file.txt - name: Commit and push to new branch + if: ${{ inputs.mode == 'ref' }} id: commit uses: devops-infra/action-commit-push@v1 with: @@ -28,6 +45,16 @@ jobs: commit_message: "[E2E] Basic commit test" target_branch: test/e2e-commit-push-basic-${{ github.run_id }} + - name: Commit and push via docker image (preview) + if: ${{ inputs.mode == 'image' }} + run: | + if [ -z "${{ inputs.image_tag }}" ]; then + echo "image_tag is required when mode=image" + exit 1 + fi + echo "Image mode placeholder for devopsinfra/action-commit-push:${{ inputs.image_tag }}" + echo "Use ref mode for authoritative validation until image-mode harness is finalized." + - name: Verify outputs run: | echo "branch_name=${{ steps.commit.outputs.branch_name }}" @@ -44,7 +71,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 @@ -52,6 +79,7 @@ jobs: run: echo "E2E prefix message test $(date -u)" > e2e-prefix-test.txt - name: Commit with custom prefix and message + if: ${{ inputs.mode == 'ref' }} id: commit uses: devops-infra/action-commit-push@v1 with: @@ -60,6 +88,16 @@ jobs: commit_message: " Custom message test" target_branch: test/e2e-commit-push-prefix-${{ github.run_id }} + - name: Commit via docker image (preview) + if: ${{ inputs.mode == 'image' }} + run: | + if [ -z "${{ inputs.image_tag }}" ]; then + echo "image_tag is required when mode=image" + exit 1 + fi + echo "Image mode placeholder for devopsinfra/action-commit-push:${{ inputs.image_tag }}" + echo "Use ref mode for authoritative validation until image-mode harness is finalized." + - name: Verify outputs run: | echo "branch_name=${{ steps.commit.outputs.branch_name }}" @@ -76,11 +114,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: Commit with no file changes to new branch + if: ${{ inputs.mode == 'ref' }} id: commit uses: devops-infra/action-commit-push@v1 with: @@ -89,6 +128,16 @@ jobs: commit_message: "[E2E] Empty commit test" target_branch: test/e2e-commit-push-empty-${{ github.run_id }} + - name: Empty commit via docker image (preview) + if: ${{ inputs.mode == 'image' }} + run: | + if [ -z "${{ inputs.image_tag }}" ]; then + echo "image_tag is required when mode=image" + exit 1 + fi + echo "Image mode placeholder for devopsinfra/action-commit-push:${{ inputs.image_tag }}" + echo "Use ref mode for authoritative validation until image-mode harness is finalized." + - name: Verify branch output run: | echo "branch_name=${{ steps.commit.outputs.branch_name }}" @@ -98,12 +147,237 @@ jobs: if: always() run: git push origin --delete test/e2e-commit-push-empty-${{ github.run_id }} 2>/dev/null || true + commit-with-timestamp-branch: + name: Commit to timestamped branch name + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Create test file + run: echo "E2E timestamp branch test $(date -u)" > e2e-timestamp-test.txt + + - name: Commit and push to timestamped branch + if: ${{ inputs.mode == 'ref' }} + id: commit + uses: devops-infra/action-commit-push@v1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + add_timestamp: "true" + commit_message: "[E2E] Timestamp branch test" + target_branch: test/e2e-commit-push-timestamp + + - name: Commit via docker image (preview) + if: ${{ inputs.mode == 'image' }} + run: | + if [ -z "${{ inputs.image_tag }}" ]; then + echo "image_tag is required when mode=image" + exit 1 + fi + echo "Image mode placeholder for devopsinfra/action-commit-push:${{ inputs.image_tag }}" + echo "Use ref mode for authoritative validation until image-mode harness is finalized." + + - name: Verify timestamped branch output + if: ${{ inputs.mode == 'ref' }} + run: | + branch_name="${{ steps.commit.outputs.branch_name }}" + echo "branch_name=${branch_name}" + [[ "${branch_name}" =~ ^test/e2e-commit-push-timestamp-[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}-[0-9]{2}-[0-9]{2}Z$ ]] + + - name: Cleanup - delete test branch + if: always() + run: | + branch_name="${{ steps.commit.outputs.branch_name }}" + if [ -n "${branch_name}" ]; then + git push origin --delete "${branch_name}" 2>/dev/null || true + fi + + commit-with-repository-path: + name: Commit from custom repository_path + runs-on: ubuntu-latest + steps: + - name: Checkout repository into custom path + uses: actions/checkout@v6 + with: + fetch-depth: 0 + path: repo + + - name: Create test file in custom path + run: echo "E2E repository path test $(date -u)" > repo/e2e-repository-path.txt + + - name: Commit and push using repository_path + if: ${{ inputs.mode == 'ref' }} + id: commit + uses: devops-infra/action-commit-push@v1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + repository_path: repo + commit_message: "[E2E] Repository path test" + target_branch: test/e2e-commit-push-path-${{ github.run_id }} + + - name: Commit via docker image (preview) + if: ${{ inputs.mode == 'image' }} + run: | + if [ -z "${{ inputs.image_tag }}" ]; then + echo "image_tag is required when mode=image" + exit 1 + fi + echo "Image mode placeholder for devopsinfra/action-commit-push:${{ inputs.image_tag }}" + echo "Use ref mode for authoritative validation until image-mode harness is finalized." + + - name: Verify repository_path output + if: ${{ inputs.mode == 'ref' }} + run: | + echo "files_changed=${{ steps.commit.outputs.files_changed }}" + test -n "${{ steps.commit.outputs.branch_name }}" + grep -Fq "e2e-repository-path.txt" <<'EOF' + ${{ steps.commit.outputs.files_changed }} + EOF + + - name: Cleanup - delete test branch + if: always() + run: git push origin --delete test/e2e-commit-push-path-${{ github.run_id }} 2>/dev/null || true + + reset-target-branch-to-base: + name: Reset target branch to base branch + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Prepare base and target branches + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + base_branch="test/e2e-commit-push-base-${{ github.run_id }}" + target_branch="test/e2e-commit-push-reset-${{ github.run_id }}" + + git checkout -b "${base_branch}" + echo "base" > e2e-reset-base.txt + git add e2e-reset-base.txt + git commit -m "[E2E] base branch for reset" + git push origin "${base_branch}" + + git checkout -b "${target_branch}" "${base_branch}" + echo "legacy" > e2e-reset-legacy.txt + git add e2e-reset-legacy.txt + git commit -m "[E2E] legacy target branch content" + git push origin "${target_branch}" + + - name: Reset target branch using action + if: ${{ inputs.mode == 'ref' }} + id: reset + uses: devops-infra/action-commit-push@v1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + target_branch: test/e2e-commit-push-reset-${{ github.run_id }} + base_branch: test/e2e-commit-push-base-${{ github.run_id }} + reset_target_branch: "true" + + - name: Reset via docker image (preview) + if: ${{ inputs.mode == 'image' }} + run: | + if [ -z "${{ inputs.image_tag }}" ]; then + echo "image_tag is required when mode=image" + exit 1 + fi + echo "Image mode placeholder for devopsinfra/action-commit-push:${{ inputs.image_tag }}" + echo "Use ref mode for authoritative validation until image-mode harness is finalized." + + - name: Verify branch reset effect + if: ${{ inputs.mode == 'ref' }} + run: | + test -n "${{ steps.reset.outputs.branch_name }}" + git fetch origin test/e2e-commit-push-reset-${{ github.run_id }} + git show origin/test/e2e-commit-push-reset-${{ github.run_id }}:e2e-reset-base.txt >/dev/null + if git show origin/test/e2e-commit-push-reset-${{ github.run_id }}:e2e-reset-legacy.txt >/dev/null 2>&1; then + echo "Expected legacy file to be removed after reset_target_branch=true" + exit 1 + fi + + - name: Cleanup - delete test branches + if: always() + run: | + git push origin --delete test/e2e-commit-push-reset-${{ github.run_id }} 2>/dev/null || true + git push origin --delete test/e2e-commit-push-base-${{ github.run_id }} 2>/dev/null || true + + rebase-conflict-fails-when-strict: + name: Rebase conflict fails when fail_on_rebase_conflict=true + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Prepare conflicting branches + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + base_branch="test/e2e-commit-push-conflict-base-${{ github.run_id }}" + target_branch="test/e2e-commit-push-conflict-target-${{ github.run_id }}" + + git checkout -b "${base_branch}" + printf 'value=one\n' > e2e-rebase-conflict.txt + git add e2e-rebase-conflict.txt + git commit -m "[E2E] base commit" + git push origin "${base_branch}" + + git checkout -b "${target_branch}" "${base_branch}" + printf 'value=target\n' > e2e-rebase-conflict.txt + git add e2e-rebase-conflict.txt + git commit -m "[E2E] target diverges" + git push origin "${target_branch}" + + git checkout "${base_branch}" + printf 'value=base\n' > e2e-rebase-conflict.txt + git add e2e-rebase-conflict.txt + git commit -m "[E2E] base diverges" + git push origin "${base_branch}" + + - name: Run action with strict rebase conflict handling + if: ${{ inputs.mode == 'ref' }} + id: strict + continue-on-error: true + uses: devops-infra/action-commit-push@v1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + target_branch: test/e2e-commit-push-conflict-target-${{ github.run_id }} + base_branch: test/e2e-commit-push-conflict-base-${{ github.run_id }} + fail_on_rebase_conflict: "true" + + - name: Conflict via docker image (preview) + if: ${{ inputs.mode == 'image' }} + run: | + if [ -z "${{ inputs.image_tag }}" ]; then + echo "image_tag is required when mode=image" + exit 1 + fi + echo "Image mode placeholder for devopsinfra/action-commit-push:${{ inputs.image_tag }}" + echo "Use ref mode for authoritative validation until image-mode harness is finalized." + + - name: Verify strict mode failure + if: ${{ inputs.mode == 'ref' }} + run: test "${{ steps.strict.outcome }}" = "failure" + + - name: Cleanup - delete test branches + if: always() + run: | + git push origin --delete test/e2e-commit-push-conflict-target-${{ github.run_id }} 2>/dev/null || true + git push origin --delete test/e2e-commit-push-conflict-base-${{ github.run_id }} 2>/dev/null || true + amend-commit: name: Amend previous commit with force-with-lease runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 @@ -121,6 +395,7 @@ jobs: run: echo "v2" >> e2e-amend-test.txt - name: Amend commit keeping original message + if: ${{ inputs.mode == 'ref' }} id: amend uses: devops-infra/action-commit-push@v1 with: @@ -129,6 +404,16 @@ jobs: no_edit: "true" force_with_lease: "true" + - name: Amend via docker image (preview) + if: ${{ inputs.mode == 'image' }} + run: | + if [ -z "${{ inputs.image_tag }}" ]; then + echo "image_tag is required when mode=image" + exit 1 + fi + echo "Image mode placeholder for devopsinfra/action-commit-push:${{ inputs.image_tag }}" + echo "Use ref mode for authoritative validation until image-mode harness is finalized." + - name: Verify branch output run: | echo "branch_name=${{ steps.amend.outputs.branch_name }}" diff --git a/.github/workflows/e2e-action-container-structure-test.yml b/.github/workflows/e2e-action-container-structure-test.yml index c43d15e..8933750 100644 --- a/.github/workflows/e2e-action-container-structure-test.yml +++ b/.github/workflows/e2e-action-container-structure-test.yml @@ -2,6 +2,22 @@ name: (E2E) Action Container Structure Test on: workflow_call: + inputs: + action_ref: + description: Git ref of devops-infra/action-container-structure-test to validate + required: false + type: string + default: v1 + mode: + description: Execution mode (ref or image) + required: false + type: string + default: ref + image_tag: + description: Image tag for docker mode (for example v1.2.3-test or v1.2.3-rc) + required: false + type: string + default: '' workflow_dispatch: permissions: @@ -13,12 +29,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Pull Alpine test image run: docker pull alpine:latest - name: Run container structure tests with text output + if: ${{ inputs.mode == 'ref' }} id: cst uses: devops-infra/action-container-structure-test@v1 with: @@ -26,6 +43,16 @@ jobs: config: tests/fixtures/container-structure-test/alpine.yml output: text + - name: Run container structure tests via docker image (preview) + if: ${{ inputs.mode == 'image' }} + run: | + if [ -z "${{ inputs.image_tag }}" ]; then + echo "image_tag is required when mode=image" + exit 1 + fi + echo "Image mode placeholder for devopsinfra/action-container-structure-test:${{ inputs.image_tag }}" + echo "Use ref mode for authoritative validation until image-mode harness is finalized." + - name: Verify test result outputs run: | echo "total=${{ steps.cst.outputs.total }}" @@ -40,12 +67,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Pull Alpine test image run: docker pull alpine:latest - name: Run container structure tests with JSON output + if: ${{ inputs.mode == 'ref' }} id: cst uses: devops-infra/action-container-structure-test@v1 with: @@ -53,6 +81,16 @@ jobs: config: tests/fixtures/container-structure-test/alpine.yml output: json + - name: Run container structure tests via docker image (preview) + if: ${{ inputs.mode == 'image' }} + run: | + if [ -z "${{ inputs.image_tag }}" ]; then + echo "image_tag is required when mode=image" + exit 1 + fi + echo "Image mode placeholder for devopsinfra/action-container-structure-test:${{ inputs.image_tag }}" + echo "Use ref mode for authoritative validation until image-mode harness is finalized." + - name: Verify test result outputs run: | echo "total=${{ steps.cst.outputs.total }}" @@ -65,12 +103,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Pull Alpine test image run: docker pull alpine:latest - name: Run container structure tests with JUnit output + if: ${{ inputs.mode == 'ref' }} id: cst uses: devops-infra/action-container-structure-test@v1 with: @@ -79,6 +118,16 @@ jobs: output: junit junit_suite_name: e2e-alpine-tests + - name: Run container structure tests via docker image (preview) + if: ${{ inputs.mode == 'image' }} + run: | + if [ -z "${{ inputs.image_tag }}" ]; then + echo "image_tag is required when mode=image" + exit 1 + fi + echo "Image mode placeholder for devopsinfra/action-container-structure-test:${{ inputs.image_tag }}" + echo "Use ref mode for authoritative validation until image-mode harness is finalized." + - name: Verify test result outputs run: | echo "total=${{ steps.cst.outputs.total }}" @@ -91,12 +140,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Pull Alpine test image run: docker pull alpine:latest - name: Run container structure tests saving report to file + if: ${{ inputs.mode == 'ref' }} id: cst uses: devops-infra/action-container-structure-test@v1 with: @@ -105,6 +155,16 @@ jobs: output: json test_report: /tmp/cst-report.json + - name: Run container structure tests via docker image (preview) + if: ${{ inputs.mode == 'image' }} + run: | + if [ -z "${{ inputs.image_tag }}" ]; then + echo "image_tag is required when mode=image" + exit 1 + fi + echo "Image mode placeholder for devopsinfra/action-container-structure-test:${{ inputs.image_tag }}" + echo "Use ref mode for authoritative validation until image-mode harness is finalized." + - name: Verify report file was created run: test -f /tmp/cst-report.json @@ -120,12 +180,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Pull Alpine test image run: docker pull alpine:latest - name: Run container structure tests with multiple config files + if: ${{ inputs.mode == 'ref' }} id: cst uses: devops-infra/action-container-structure-test@v1 with: @@ -135,6 +196,16 @@ jobs: tests/fixtures/container-structure-test/alpine-extended.yml output: text + - name: Run container structure tests via docker image (preview) + if: ${{ inputs.mode == 'image' }} + run: | + if [ -z "${{ inputs.image_tag }}" ]; then + echo "image_tag is required when mode=image" + exit 1 + fi + echo "Image mode placeholder for devopsinfra/action-container-structure-test:${{ inputs.image_tag }}" + echo "Use ref mode for authoritative validation until image-mode harness is finalized." + - name: Verify combined test results run: | echo "total=${{ steps.cst.outputs.total }}" @@ -143,3 +214,54 @@ jobs: echo "exit_code=${{ steps.cst.outputs.exit_code }}" test "${{ steps.cst.outputs.exit_code }}" = "0" test "${{ steps.cst.outputs.failed }}" = "0" + + metadata-platform-runtime: + name: Test with metadata, platform, and runtime inputs + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Pull Alpine test image + run: docker pull alpine:latest + + - name: Create metadata file + run: | + cat > /tmp/cst-metadata.json << 'EOF' + { + "config": { + "Env": ["FOO=bar"] + } + } + EOF + + - name: Run container structure tests with metadata/platform/runtime + if: ${{ inputs.mode == 'ref' }} + id: cst + uses: devops-infra/action-container-structure-test@v1 + with: + image: alpine:latest + config: tests/fixtures/container-structure-test/alpine.yml + driver: docker + platform: linux/amd64 + runtime: runc + metadata: /tmp/cst-metadata.json + output: text + + - name: Run container structure tests via docker image (preview) + if: ${{ inputs.mode == 'image' }} + run: | + if [ -z "${{ inputs.image_tag }}" ]; then + echo "image_tag is required when mode=image" + exit 1 + fi + echo "Image mode placeholder for devopsinfra/action-container-structure-test:${{ inputs.image_tag }}" + echo "Use ref mode for authoritative validation until image-mode harness is finalized." + + - name: Verify test result outputs + run: | + echo "total=${{ steps.cst.outputs.total }}" + echo "passed=${{ steps.cst.outputs.passed }}" + echo "failed=${{ steps.cst.outputs.failed }}" + echo "exit_code=${{ steps.cst.outputs.exit_code }}" + test "${{ steps.cst.outputs.exit_code }}" = "0" diff --git a/.github/workflows/e2e-action-format-hcl.yml b/.github/workflows/e2e-action-format-hcl.yml index c1b4d6a..b389549 100644 --- a/.github/workflows/e2e-action-format-hcl.yml +++ b/.github/workflows/e2e-action-format-hcl.yml @@ -2,6 +2,22 @@ name: (E2E) Action Format HCL on: workflow_call: + inputs: + action_ref: + description: Git ref of devops-infra/action-format-hcl to validate + required: false + type: string + default: v1 + mode: + description: Execution mode (ref or image) + required: false + type: string + default: ref + image_tag: + description: Image tag for docker mode (for example v1.2.3-test or v1.2.3-rc) + required: false + type: string + default: '' workflow_dispatch: permissions: @@ -13,7 +29,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Create well-formatted Terraform files run: | @@ -40,17 +56,35 @@ jobs: EOF - name: Check already-formatted files + if: ${{ inputs.mode == 'ref' }} uses: devops-infra/action-format-hcl@v1 with: check: "true" dir: tests/e2e-hcl-clean + - name: Check via docker image (preview) + if: ${{ inputs.mode == 'image' }} + env: + ACTION_IMAGE: devopsinfra/action-format-hcl:${{ inputs.image_tag }} + run: | + if [ -z "${{ inputs.image_tag }}" ]; then + echo "image_tag is required when mode=image" + exit 1 + fi + docker pull "${ACTION_IMAGE}" + docker run --rm \ + -e INPUT_CHECK=true \ + -e INPUT_DIR=tests/e2e-hcl-clean \ + -v "$PWD:/github/workspace" \ + -w /github/workspace \ + "${ACTION_IMAGE}" + format-write-mode: name: Write mode formats and rewrites files runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Create Terraform files for write-mode formatting run: | @@ -71,6 +105,7 @@ jobs: EOF - name: Run format in write mode + if: ${{ inputs.mode == 'ref' }} id: format uses: devops-infra/action-format-hcl@v1 with: @@ -78,15 +113,38 @@ jobs: diff: "true" dir: tests/e2e-hcl-write + - name: Run format via docker image (preview) + if: ${{ inputs.mode == 'image' }} + env: + ACTION_IMAGE: devopsinfra/action-format-hcl:${{ inputs.image_tag }} + run: | + if [ -z "${{ inputs.image_tag }}" ]; then + echo "image_tag is required when mode=image" + exit 1 + fi + docker pull "${ACTION_IMAGE}" + docker run --rm \ + -e INPUT_WRITE=true \ + -e INPUT_DIFF=true \ + -e INPUT_DIR=tests/e2e-hcl-write \ + -v "$PWD:/github/workspace" \ + -w /github/workspace \ + "${ACTION_IMAGE}" + - name: Show files changed output + if: ${{ inputs.mode == 'ref' }} run: echo "files_changed=${{ steps.format.outputs.files_changed }}" + - name: Verify image mode execution + if: ${{ inputs.mode == 'image' }} + run: test -f tests/e2e-hcl-write/main.tf + format-check-malformed-files: name: Check mode on malformed files reports failure runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Create malformed Terraform file run: | @@ -94,6 +152,7 @@ jobs: printf 'variable "foo" {\ntype = string\ndescription="bad spacing"\n}\n' > tests/e2e-hcl-malformed/malformed.tf - name: Check malformed files - expected to fail + if: ${{ inputs.mode == 'ref' }} id: check uses: devops-infra/action-format-hcl@v1 continue-on-error: true @@ -101,7 +160,29 @@ jobs: check: "true" dir: tests/e2e-hcl-malformed + - name: Check malformed files via docker image - expected to fail + if: ${{ inputs.mode == 'image' }} + env: + ACTION_IMAGE: devopsinfra/action-format-hcl:${{ inputs.image_tag }} + run: | + if [ -z "${{ inputs.image_tag }}" ]; then + echo "image_tag is required when mode=image" + exit 1 + fi + docker pull "${ACTION_IMAGE}" + set +e + docker run --rm \ + -e INPUT_CHECK=true \ + -e INPUT_DIR=tests/e2e-hcl-malformed \ + -v "$PWD:/github/workspace" \ + -w /github/workspace \ + "${ACTION_IMAGE}" + rc=$? + set -e + test "$rc" -ne 0 + - name: Verify action detected formatting issues + if: ${{ inputs.mode == 'ref' }} run: test "${{ steps.check.outcome }}" = "failure" format-list-with-diff: @@ -109,7 +190,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Create Terraform files for list/diff mode run: | @@ -123,6 +204,7 @@ jobs: EOF - name: Run format in list and diff mode + if: ${{ inputs.mode == 'ref' }} id: format uses: devops-infra/action-format-hcl@v1 with: @@ -131,5 +213,29 @@ jobs: write: "false" dir: tests/e2e-hcl-list + - name: Run format via docker image (preview) + if: ${{ inputs.mode == 'image' }} + env: + ACTION_IMAGE: devopsinfra/action-format-hcl:${{ inputs.image_tag }} + run: | + if [ -z "${{ inputs.image_tag }}" ]; then + echo "image_tag is required when mode=image" + exit 1 + fi + docker pull "${ACTION_IMAGE}" + docker run --rm \ + -e INPUT_LIST=true \ + -e INPUT_DIFF=true \ + -e INPUT_WRITE=false \ + -e INPUT_DIR=tests/e2e-hcl-list \ + -v "$PWD:/github/workspace" \ + -w /github/workspace \ + "${ACTION_IMAGE}" + - name: Show files changed output + if: ${{ inputs.mode == 'ref' }} run: echo "files_changed=${{ steps.format.outputs.files_changed }}" + + - name: Verify image mode execution + if: ${{ inputs.mode == 'image' }} + run: test -f tests/e2e-hcl-list/variables.tf diff --git a/.github/workflows/e2e-action-pull-request.yml b/.github/workflows/e2e-action-pull-request.yml index 200bea9..40cd745 100644 --- a/.github/workflows/e2e-action-pull-request.yml +++ b/.github/workflows/e2e-action-pull-request.yml @@ -2,6 +2,22 @@ name: (E2E) Action Pull Request on: workflow_call: + inputs: + action_ref: + description: Git ref of devops-infra/action-pull-request to validate + required: false + type: string + default: v1 + mode: + description: Execution mode (ref or image) + required: false + type: string + default: ref + image_tag: + description: Image tag for docker mode (for example v1.2.3-test or v1.2.3-rc) + required: false + type: string + default: '' workflow_dispatch: permissions: @@ -15,7 +31,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 @@ -30,6 +46,7 @@ jobs: git push origin test/e2e-pr-basic-${{ github.run_id }} - name: Create pull request + if: ${{ inputs.mode == 'ref' }} id: pr uses: devops-infra/action-pull-request@v1 with: @@ -38,6 +55,16 @@ jobs: body: "Automated end-to-end test pull request. Will be closed automatically." target_branch: master + - name: Create pull request via docker image (preview) + if: ${{ inputs.mode == 'image' }} + run: | + if [ -z "${{ inputs.image_tag }}" ]; then + echo "image_tag is required when mode=image" + exit 1 + fi + echo "Image mode placeholder for devopsinfra/action-pull-request:${{ inputs.image_tag }}" + echo "Use ref mode for authoritative validation until image-mode harness is finalized." + - name: Verify outputs run: | echo "url=${{ steps.pr.outputs.url }}" @@ -61,7 +88,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 @@ -76,6 +103,7 @@ jobs: git push origin test/e2e-pr-custom-${{ github.run_id }} - name: Create pull request with custom title and body + if: ${{ inputs.mode == 'ref' }} id: pr uses: devops-infra/action-pull-request@v1 with: @@ -88,6 +116,16 @@ jobs: target_branch: master allow_no_diff: "false" + - name: Create pull request via docker image (preview) + if: ${{ inputs.mode == 'image' }} + run: | + if [ -z "${{ inputs.image_tag }}" ]; then + echo "image_tag is required when mode=image" + exit 1 + fi + echo "Image mode placeholder for devopsinfra/action-pull-request:${{ inputs.image_tag }}" + echo "Use ref mode for authoritative validation until image-mode harness is finalized." + - name: Verify outputs run: | echo "url=${{ steps.pr.outputs.url }}" @@ -111,7 +149,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository into custom path - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 path: work/repo @@ -128,6 +166,7 @@ jobs: git push origin test/e2e-pr-repo-path-${{ github.run_id }} - name: Create pull request with repository and repository_path + if: ${{ inputs.mode == 'ref' }} id: pr uses: devops-infra/action-pull-request@v1 with: @@ -138,6 +177,16 @@ jobs: body: "Automated end-to-end test for repository_path input." target_branch: master + - name: Create pull request via docker image (preview) + if: ${{ inputs.mode == 'image' }} + run: | + if [ -z "${{ inputs.image_tag }}" ]; then + echo "image_tag is required when mode=image" + exit 1 + fi + echo "Image mode placeholder for devopsinfra/action-pull-request:${{ inputs.image_tag }}" + echo "Use ref mode for authoritative validation until image-mode harness is finalized." + - name: Verify outputs run: | echo "url=${{ steps.pr.outputs.url }}" @@ -162,7 +211,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 @@ -177,6 +226,7 @@ jobs: git push origin test/e2e-pr-draft-${{ github.run_id }} - name: Create draft pull request with diff injection + if: ${{ inputs.mode == 'ref' }} id: pr uses: devops-infra/action-pull-request@v1 with: @@ -192,6 +242,16 @@ jobs: draft: "true" get_diff: "true" + - name: Create draft pull request via docker image (preview) + if: ${{ inputs.mode == 'image' }} + run: | + if [ -z "${{ inputs.image_tag }}" ]; then + echo "image_tag is required when mode=image" + exit 1 + fi + echo "Image mode placeholder for devopsinfra/action-pull-request:${{ inputs.image_tag }}" + echo "Use ref mode for authoritative validation until image-mode harness is finalized." + - name: Verify outputs run: | echo "url=${{ steps.pr.outputs.url }}" diff --git a/.github/workflows/e2e-action-template-action.yml b/.github/workflows/e2e-action-template-action.yml new file mode 100644 index 0000000..f53356d --- /dev/null +++ b/.github/workflows/e2e-action-template-action.yml @@ -0,0 +1,81 @@ +name: (E2E) Template Action + +on: + workflow_call: + inputs: + action_ref: + description: Git ref of devops-infra/template-action to validate + required: false + type: string + default: v1 + mode: + description: Execution mode (ref or image) + required: false + type: string + default: ref + image_tag: + description: Image tag for docker mode (for example v1.2.3-test or v1.2.3-rc) + required: false + type: string + default: '' + workflow_dispatch: + +permissions: + contents: read + +jobs: + template-action-input-output: + name: Validate template-action input/output contract + runs-on: ubuntu-latest + steps: + - name: Run template action in ref mode + if: ${{ inputs.mode == 'ref' }} + id: action + uses: devops-infra/template-action@v1 + with: + foobar: e2e-template-value + debug: 'false' + + - name: Run template action via docker image (preview) + if: ${{ inputs.mode == 'image' }} + run: | + if [ -z "${{ inputs.image_tag }}" ]; then + echo "image_tag is required when mode=image" + exit 1 + fi + echo "Image mode placeholder for devopsinfra/template-action:${{ inputs.image_tag }}" + echo "Use ref mode for authoritative validation until image-mode harness is finalized." + + - name: Verify outputs + if: ${{ inputs.mode == 'ref' }} + run: | + test "${{ steps.action.outputs.foobar }}" = "e2e-template-value" + test "${{ steps.action.outputs.barfoo }}" = "e2e-template-value" + + template-action-debug: + name: Validate template-action debug mode + runs-on: ubuntu-latest + steps: + - name: Run template action with debug enabled + if: ${{ inputs.mode == 'ref' }} + id: action + uses: devops-infra/template-action@v1 + with: + foobar: e2e-debug-value + debug: 'true' + + - name: Run template action via docker image (preview) + if: ${{ inputs.mode == 'image' }} + run: | + if [ -z "${{ inputs.image_tag }}" ]; then + echo "image_tag is required when mode=image" + exit 1 + fi + echo "Image mode placeholder for devopsinfra/template-action:${{ inputs.image_tag }}" + echo "Use ref mode for authoritative validation until image-mode harness is finalized." + + - name: Verify outputs + if: ${{ inputs.mode == 'ref' }} + run: | + test "${{ steps.action.outputs.foobar }}" = "e2e-debug-value" + test "${{ steps.action.outputs.barfoo }}" = "e2e-debug-value" diff --git a/.github/workflows/e2e-action-terraform-copy-vars.yml b/.github/workflows/e2e-action-terraform-copy-vars.yml index ad2e31f..dfb1916 100644 --- a/.github/workflows/e2e-action-terraform-copy-vars.yml +++ b/.github/workflows/e2e-action-terraform-copy-vars.yml @@ -2,6 +2,22 @@ name: (E2E) Action Terraform Copy Vars on: workflow_call: + inputs: + action_ref: + description: Git ref of devops-infra/action-terraform-copy-vars to validate + required: false + type: string + default: v1 + mode: + description: Execution mode (ref or image) + required: false + type: string + default: ref + image_tag: + description: Image tag for docker mode (for example v1.2.3-test or v1.2.3-rc) + required: false + type: string + default: '' workflow_dispatch: permissions: @@ -13,7 +29,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Create Terraform module structure with all-variables file run: | @@ -54,6 +70,7 @@ jobs: EOF - name: Copy variables from central file to all modules + if: ${{ inputs.mode == 'ref' }} id: copy uses: devops-infra/action-terraform-copy-vars@v1 with: @@ -62,15 +79,39 @@ jobs: all_vars_file: all-variables.tf fail_on_missing: "false" + - name: Copy vars via docker image (preview) + if: ${{ inputs.mode == 'image' }} + env: + ACTION_IMAGE: devopsinfra/action-terraform-copy-vars:${{ inputs.image_tag }} + run: | + if [ -z "${{ inputs.image_tag }}" ]; then + echo "image_tag is required when mode=image" + exit 1 + fi + docker pull "${ACTION_IMAGE}" + docker run --rm \ + -e INPUT_DIRS_WITH_MODULES=terraform \ + -e INPUT_FILES_WITH_VARS=variables.tf \ + -e INPUT_ALL_VARS_FILE=all-variables.tf \ + -e INPUT_FAIL_ON_MISSING=false \ + -v "$PWD:/github/workspace" \ + -w /github/workspace \ + "${ACTION_IMAGE}" + - name: Show files changed output + if: ${{ inputs.mode == 'ref' }} run: echo "files_changed=${{ steps.copy.outputs.files_changed }}" + - name: Verify image mode execution + if: ${{ inputs.mode == 'image' }} + run: grep -q 'variable "env"' terraform/module_a/variables.tf + copy-vars-custom-paths: name: Copy variables with custom directory and file paths runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Create custom Terraform structure run: | @@ -104,6 +145,7 @@ jobs: EOF - name: Copy variables using custom paths + if: ${{ inputs.mode == 'ref' }} id: copy uses: devops-infra/action-terraform-copy-vars@v1 with: @@ -112,15 +154,39 @@ jobs: all_vars_file: infra/all-vars.tf fail_on_missing: "false" + - name: Copy vars via docker image (preview) + if: ${{ inputs.mode == 'image' }} + env: + ACTION_IMAGE: devopsinfra/action-terraform-copy-vars:${{ inputs.image_tag }} + run: | + if [ -z "${{ inputs.image_tag }}" ]; then + echo "image_tag is required when mode=image" + exit 1 + fi + docker pull "${ACTION_IMAGE}" + docker run --rm \ + -e INPUT_DIRS_WITH_MODULES=infra/services \ + -e INPUT_FILES_WITH_VARS=vars.tf \ + -e INPUT_ALL_VARS_FILE=infra/all-vars.tf \ + -e INPUT_FAIL_ON_MISSING=false \ + -v "$PWD:/github/workspace" \ + -w /github/workspace \ + "${ACTION_IMAGE}" + - name: Show files changed output + if: ${{ inputs.mode == 'ref' }} run: echo "files_changed=${{ steps.copy.outputs.files_changed }}" + - name: Verify image mode execution + if: ${{ inputs.mode == 'image' }} + run: grep -q 'variable "version_tag"' infra/services/api/vars.tf + copy-vars-fail-on-missing: name: Fail when module uses variable missing from central file runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Create Terraform structure with missing variable run: | @@ -148,6 +214,7 @@ jobs: EOF - name: Run copy-vars with fail_on_missing - expected to fail + if: ${{ inputs.mode == 'ref' }} id: copy uses: devops-infra/action-terraform-copy-vars@v1 continue-on-error: true @@ -157,5 +224,29 @@ jobs: all_vars_file: all-variables.tf fail_on_missing: "true" + - name: Copy vars via docker image (preview) + if: ${{ inputs.mode == 'image' }} + env: + ACTION_IMAGE: devopsinfra/action-terraform-copy-vars:${{ inputs.image_tag }} + run: | + if [ -z "${{ inputs.image_tag }}" ]; then + echo "image_tag is required when mode=image" + exit 1 + fi + docker pull "${ACTION_IMAGE}" + set +e + docker run --rm \ + -e INPUT_DIRS_WITH_MODULES=terraform \ + -e INPUT_FILES_WITH_VARS=variables.tf \ + -e INPUT_ALL_VARS_FILE=all-variables.tf \ + -e INPUT_FAIL_ON_MISSING=true \ + -v "$PWD:/github/workspace" \ + -w /github/workspace \ + "${ACTION_IMAGE}" + rc=$? + set -e + test "$rc" -ne 0 + - name: Verify action failed on missing variable + if: ${{ inputs.mode == 'ref' }} run: test "${{ steps.copy.outcome }}" = "failure" diff --git a/.github/workflows/e2e-action-terraform-validate.yml b/.github/workflows/e2e-action-terraform-validate.yml index 5cc6267..970aa43 100644 --- a/.github/workflows/e2e-action-terraform-validate.yml +++ b/.github/workflows/e2e-action-terraform-validate.yml @@ -2,6 +2,22 @@ name: (E2E) Action Terraform Validate on: workflow_call: + inputs: + action_ref: + description: Git ref of devops-infra/action-terraform-validate to validate + required: false + type: string + default: v1 + mode: + description: Execution mode (ref or image) + required: false + type: string + default: ref + image_tag: + description: Image tag for docker mode (for example v1.2.3-test or v1.2.3-rc) + required: false + type: string + default: '' workflow_dispatch: permissions: @@ -13,7 +29,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Create valid Terraform module run: | @@ -40,16 +56,33 @@ jobs: EOF - name: Validate all Terraform modules + if: ${{ inputs.mode == 'ref' }} uses: devops-infra/action-terraform-validate@v1 with: dir_filter: terraform + - name: Validate via docker image (preview) + if: ${{ inputs.mode == 'image' }} + env: + ACTION_IMAGE: devopsinfra/action-terraform-validate:${{ inputs.image_tag }} + run: | + if [ -z "${{ inputs.image_tag }}" ]; then + echo "image_tag is required when mode=image" + exit 1 + fi + docker pull "${ACTION_IMAGE}" + docker run --rm \ + -e INPUT_DIR_FILTER=terraform \ + -v "$PWD:/github/workspace" \ + -w /github/workspace \ + "${ACTION_IMAGE}" + validate-with-dir-filter: name: Validate with explicit directory filter runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Create multiple Terraform modules run: | @@ -79,6 +112,23 @@ jobs: EOF - name: Validate only module_a + if: ${{ inputs.mode == 'ref' }} uses: devops-infra/action-terraform-validate@v1 with: dir_filter: terraform/module_a + + - name: Validate via docker image (preview) + if: ${{ inputs.mode == 'image' }} + env: + ACTION_IMAGE: devopsinfra/action-terraform-validate:${{ inputs.image_tag }} + run: | + if [ -z "${{ inputs.image_tag }}" ]; then + echo "image_tag is required when mode=image" + exit 1 + fi + docker pull "${ACTION_IMAGE}" + docker run --rm \ + -e INPUT_DIR_FILTER=terraform/module_a \ + -v "$PWD:/github/workspace" \ + -w /github/workspace \ + "${ACTION_IMAGE}" diff --git a/.github/workflows/e2e-action-tflint.yml b/.github/workflows/e2e-action-tflint.yml index 92ba722..bebb3a1 100644 --- a/.github/workflows/e2e-action-tflint.yml +++ b/.github/workflows/e2e-action-tflint.yml @@ -2,6 +2,22 @@ name: (E2E) Action TFLint on: workflow_call: + inputs: + action_ref: + description: Git ref of devops-infra/action-tflint to validate + required: false + type: string + default: v1 + mode: + description: Execution mode (ref or image) + required: false + type: string + default: ref + image_tag: + description: Image tag for docker mode (for example v1.2.3-test or v1.2.3-rc) + required: false + type: string + default: '' workflow_dispatch: permissions: @@ -13,7 +29,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Create valid Terraform module run: | @@ -34,18 +50,37 @@ jobs: EOF - name: Run TFLint on all Terraform directories + if: ${{ inputs.mode == 'ref' }} uses: devops-infra/action-tflint@v1 with: dir_filter: terraform run_init: "false" fail_on_changes: "false" + - name: Run TFLint from docker image (preview) + if: ${{ inputs.mode == 'image' }} + env: + ACTION_IMAGE: devopsinfra/action-tflint:${{ inputs.image_tag }} + run: | + if [ -z "${{ inputs.image_tag }}" ]; then + echo "image_tag is required when mode=image" + exit 1 + fi + docker pull "${ACTION_IMAGE}" + docker run --rm \ + -e INPUT_DIR_FILTER=terraform \ + -e INPUT_RUN_INIT=false \ + -e INPUT_FAIL_ON_CHANGES=false \ + -v "$PWD:/github/workspace" \ + -w /github/workspace \ + "${ACTION_IMAGE}" + tflint-with-dir-filter: name: TFLint with specific directory filter runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Create multiple Terraform modules run: | @@ -75,18 +110,37 @@ jobs: EOF - name: Run TFLint only on aws module + if: ${{ inputs.mode == 'ref' }} uses: devops-infra/action-tflint@v1 with: dir_filter: terraform/module_aws run_init: "false" fail_on_changes: "false" + - name: Run TFLint from docker image (preview) + if: ${{ inputs.mode == 'image' }} + env: + ACTION_IMAGE: devopsinfra/action-tflint:${{ inputs.image_tag }} + run: | + if [ -z "${{ inputs.image_tag }}" ]; then + echo "image_tag is required when mode=image" + exit 1 + fi + docker pull "${ACTION_IMAGE}" + docker run --rm \ + -e INPUT_DIR_FILTER=terraform/module_aws \ + -e INPUT_RUN_INIT=false \ + -e INPUT_FAIL_ON_CHANGES=false \ + -v "$PWD:/github/workspace" \ + -w /github/workspace \ + "${ACTION_IMAGE}" + tflint-no-fail-on-changes: name: TFLint with fail_on_changes disabled runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Create Terraform module with lint warning run: | @@ -105,8 +159,138 @@ jobs: EOF - name: Run TFLint without failing on findings + if: ${{ inputs.mode == 'ref' }} uses: devops-infra/action-tflint@v1 with: dir_filter: terraform/module_warn run_init: "false" fail_on_changes: "false" + + - name: Run TFLint from docker image (preview) + if: ${{ inputs.mode == 'image' }} + env: + ACTION_IMAGE: devopsinfra/action-tflint:${{ inputs.image_tag }} + run: | + if [ -z "${{ inputs.image_tag }}" ]; then + echo "image_tag is required when mode=image" + exit 1 + fi + docker pull "${ACTION_IMAGE}" + docker run --rm \ + -e INPUT_DIR_FILTER=terraform/module_warn \ + -e INPUT_RUN_INIT=false \ + -e INPUT_FAIL_ON_CHANGES=false \ + -v "$PWD:/github/workspace" \ + -w /github/workspace \ + "${ACTION_IMAGE}" + + tflint-with-custom-config: + name: TFLint with explicit tflint_config + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Create Terraform module and custom TFLint config + run: | + mkdir -p terraform/module_custom + cat > terraform/module_custom/main.tf << 'EOF' + terraform { + required_version = ">= 1.5.0" + } + + variable "name" { + type = string + } + + output "name" { + value = var.name + } + EOF + + cat > terraform/.tflint.custom.hcl << 'EOF' + config { + module = false + force = false + } + EOF + + - name: Run TFLint with custom config path + if: ${{ inputs.mode == 'ref' }} + uses: devops-infra/action-tflint@v1 + with: + dir_filter: terraform/module_custom + run_init: "false" + fail_on_changes: "false" + tflint_config: terraform/.tflint.custom.hcl + + - name: Run TFLint from docker image (preview) + if: ${{ inputs.mode == 'image' }} + env: + ACTION_IMAGE: devopsinfra/action-tflint:${{ inputs.image_tag }} + run: | + if [ -z "${{ inputs.image_tag }}" ]; then + echo "image_tag is required when mode=image" + exit 1 + fi + docker pull "${ACTION_IMAGE}" + docker run --rm \ + -e INPUT_DIR_FILTER=terraform/module_custom \ + -e INPUT_RUN_INIT=false \ + -e INPUT_FAIL_ON_CHANGES=false \ + -e INPUT_TFLINT_CONFIG=terraform/.tflint.custom.hcl \ + -v "$PWD:/github/workspace" \ + -w /github/workspace \ + "${ACTION_IMAGE}" + + tflint-with-custom-params: + name: TFLint with custom tflint_params + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Create Terraform module + run: | + mkdir -p terraform/module_params + cat > terraform/module_params/main.tf << 'EOF' + terraform { + required_version = ">= 1.5.0" + } + + variable "name" { + type = string + } + + output "name" { + value = var.name + } + EOF + + - name: Run TFLint with custom params + if: ${{ inputs.mode == 'ref' }} + uses: devops-infra/action-tflint@v1 + with: + dir_filter: terraform/module_params + run_init: "false" + fail_on_changes: "false" + tflint_params: "--minimum-failure-severity=error" + + - name: Run TFLint from docker image (preview) + if: ${{ inputs.mode == 'image' }} + env: + ACTION_IMAGE: devopsinfra/action-tflint:${{ inputs.image_tag }} + run: | + if [ -z "${{ inputs.image_tag }}" ]; then + echo "image_tag is required when mode=image" + exit 1 + fi + docker pull "${ACTION_IMAGE}" + docker run --rm \ + -e INPUT_DIR_FILTER=terraform/module_params \ + -e INPUT_RUN_INIT=false \ + -e INPUT_FAIL_ON_CHANGES=false \ + -e INPUT_TFLINT_PARAMS=--minimum-failure-severity=error \ + -v "$PWD:/github/workspace" \ + -w /github/workspace \ + "${ACTION_IMAGE}" diff --git a/.gitignore b/.gitignore index 22f7e67..56852be 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,12 @@ build/ dist/ *.egg-info/ *.pyc -__py +__pycache__/ + +# Local test artifacts +*.tf +*.tfvars +terraform/ +infra/ +tests/e2e-hcl-*/ +e2e-*.txt diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c03960b..1cb5dfb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,3 +39,8 @@ repos: entry: bash -lc 'docker run --rm -v "$PWD:/work" -w /work cytopia/yamllint -c .yamllint.yml "$@"' -- language: system files: \.(yml|yaml)$ + - id: pylint + name: pylint + entry: python3 -m pylint --rcfile .pylintrc + language: system + files: \.py$ diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..a28fbaf --- /dev/null +++ b/.pylintrc @@ -0,0 +1 @@ +[MASTER] diff --git a/README.md b/README.md index 596a8ba..04e62ef 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,126 @@ -# Universal template for organizational repository +# End-to-End Tests Framework +Repository-level framework used to validate `devops-infra` automation end-to-end, with a focus on GitHub Actions behavior in real workflow runs. -## 📊 Badges -[ -![GitHub repo](https://img.shields.io/badge/GitHub-devops--infra%2Ftemplate--repository-blueviolet.svg?style=plastic&logo=github) -![GitHub last commit](https://img.shields.io/github/last-commit/devops-infra/template-repository?color=blueviolet&logo=github&style=plastic&label=Last%20commit) -![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/devops-infra/template-repository?color=blueviolet&label=Code%20size&style=plastic&logo=github) -![GitHub license](https://img.shields.io/github/license/devops-infra/template-repository?color=blueviolet&logo=github&style=plastic&label=License) -](https://github.com/devops-infra/template-repository "shields.io") +## Scope -## Forking -To publish images from a fork, set these variables so Task uses your registry identities: -`DOCKER_USERNAME`, `DOCKER_ORG_NAME`, `GITHUB_USERNAME`, `GITHUB_ORG_NAME`. +- Executes reusable and action-specific E2E workflows in this repository. +- Verifies outputs, expected failures, and integration behavior against live GitHub runtime. +- Provides a stable place to add regression tests before rolling changes organization-wide. + +## Covered Actions and Test Types + +| Action | Workflow | Test Coverage | +|------------------------------------------------|-------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------| +| `devops-infra/action-commit-push` | `.github/workflows/e2e-action-commit-push.yml` | branch creation/push, custom message/prefix, empty commit mode, amend with force-with-lease, output verification, cleanup | +| `devops-infra/action-pull-request` | `.github/workflows/e2e-action-pull-request.yml` | PR creation/update paths, custom title/body, draft + `get_diff`, `repository` + `repository_path`, output verification, cleanup | +| `devops-infra/action-format-hcl` | `.github/workflows/e2e-action-format-hcl.yml` | check mode pass/fail, write mode, list/diff mode, malformed input detection | +| `devops-infra/action-container-structure-test` | `.github/workflows/e2e-action-container-structure-test.yml` | text/json/junit output modes, report file creation, multi-config execution, output counters | +| `devops-infra/action-terraform-copy-vars` | `.github/workflows/e2e-action-terraform-copy-vars.yml` | variable propagation across modules, custom path inputs, strict missing-variable failure mode | +| `devops-infra/action-terraform-validate` | `.github/workflows/e2e-action-terraform-validate.yml` | valid module validation, scoped validation via `dir_filter` | +| `devops-infra/action-tflint` | `.github/workflows/e2e-action-tflint.yml` | lint execution across modules, scoped lint via `dir_filter`, non-blocking findings mode | +| `devops-infra/template-action` | `.github/workflows/e2e-action-template-action.yml` | baseline template behavior validation, output contract checks, debug-mode execution | + +## Workflow Orchestration + +- Main orchestrator: `.github/workflows/cron-e2e-tests.yml` +- Triggers: + - weekly cron schedule + - manual dispatch (`workflow_dispatch`) +- Executes all action-focused E2E workflows via reusable `workflow_call` jobs. + +## Local Development + +Prerequisites: + +- `task` +- `docker` +- `gh` (authenticated) + +Common commands: -Two supported options (environment variables take precedence over `.env`): ```bash -# .env (local only, not committed) -DOCKER_USERNAME=your-dockerhub-user -DOCKER_ORG_NAME=your-dockerhub-org -GITHUB_USERNAME=your-github-user -GITHUB_ORG_NAME=your-github-org +task lint +task pre-commit +task e2e:list-workflows +task e2e:run WORKFLOW=e2e-action-pull-request.yml +task e2e:run:all +task test:coverage:report +task test:coverage:gate ``` +Useful follow-up commands: + +```bash +task e2e:view-latest WORKFLOW=e2e-action-pull-request.yml +task e2e:watch RUN_ID= +``` + +## Manual Workflow Runs: Permissions and Secrets + +When triggering workflows manually with `gh workflow run`, ensure: + +- Your local GitHub CLI token has `repo` and `workflow` scopes. +- `gh auth status` is healthy for the same GitHub account that can run workflows in this repository. +- Workflow job permissions remain enabled for tested actions: + - `contents: write` for branch/commit operations (`action-commit-push`, `action-pull-request` tests) + - `pull-requests: write` and `issues: write` for PR lifecycle operations (`action-pull-request` tests) + - `contents: read` for read-only action workflows (`action-format-hcl`, `action-tflint`, `action-terraform-*`, `action-container-structure-test`) + +Manual dispatch examples: + ```bash -# Shell override -DOCKER_USERNAME=your-dockerhub-user \ -DOCKER_ORG_NAME=your-dockerhub-org \ -GITHUB_USERNAME=your-github-user \ -GITHUB_ORG_NAME=your-github-org \ -task docker:build +task e2e:run WORKFLOW=e2e-action-commit-push.yml +task e2e:run:all +``` + +This repository primarily relies on the built-in `${{ secrets.GITHUB_TOKEN }}` in workflow runs. +If future scenarios require elevated credentials, define additional secrets in repository settings and document them in the specific workflow file. + +## Input Coverage Gate + +- Coverage report: `task test:coverage:report` +- Strict gate: `task test:coverage:gate` +- Baseline file for currently accepted uncovered inputs: `tests/coverage-baseline.json` + +The strict gate fails only when newly uncovered inputs appear outside the baseline. + +## Reusable Workflow Usage in Action Repositories + +Each `e2e-action-*.yml` workflow supports `workflow_call`, so action repositories can reuse this framework for pre-merge checks. + +Current org-wide automation wiring: + +- Pull request flow (`reusable-auto-pull-request-create.yml`) calls action-specific E2E workflows for `action-*` repositories. +- Release branch prepare flow (`reusable-manual-release-branch-prepare.yml`) calls action-specific E2E workflows against `release/*` refs and `-rc` tags. +- Release create flow (`reusable-auto-release-create.yml`) calls action-specific E2E workflows against production release tags/images. + +Recommended pre-merge strategy: + +- Run E2E with action refs that point to the PR under test (branch or SHA). +- Run image-tag verification stages for `-test` and `-rc` tags after image publication in release pipelines. + +Current image-mode implementation: + +- `e2e-action-format-hcl.yml` supports executable `mode: image` using `image_tag`. +- `e2e-action-tflint.yml` supports executable `mode: image` using `image_tag`. +- `e2e-action-terraform-validate.yml` supports executable `mode: image` using `image_tag`. +- `e2e-action-terraform-copy-vars.yml` supports executable `mode: image` using `image_tag`. +- `e2e-action-container-structure-test.yml` currently uses `mode: ref` as authoritative path in reusable CI flows. +- `e2e-action-commit-push.yml` and `e2e-action-pull-request.yml` use `mode: ref` as authoritative path in reusable CI flows. + +Example caller from another action repository: + +```yaml +jobs: + e2e-pr-validation: + uses: devops-infra/end-to-end-tests/.github/workflows/e2e-action-pull-request.yml@master + with: + mode: image + image_tag: v1.2.3-test ``` -Recommended setup: -- Local development: use a `.env` file. -- GitHub Actions: set repo variables for the four values above, and secrets for `DOCKER_TOKEN` and `GITHUB_TOKEN`. +## Notes -Publish images without a release: -- Run the `(Manual) Release Create` workflow with `build_only: true` to build and push images without tagging a release. +- E2E workflows intentionally create temporary test branches and pull requests and then clean them up. +- Use this repository to validate behavior before promoting changes in action repositories or reusable org workflows. diff --git a/Taskfile.cicd.yml b/Taskfile.cicd.yml index 82fe5b3..78ef313 100644 --- a/Taskfile.cicd.yml +++ b/Taskfile.cicd.yml @@ -21,6 +21,7 @@ tasks: - task: lint:actionlint - task: lint:shellcheck - task: lint:yamllint + - task: lint:pylint lint:actionlint: desc: Lint GitHub Actions workflows with actionlint @@ -37,6 +38,46 @@ tasks: cmds: - task: scripts:lint:yamllint + lint:pylint: + desc: Lint Python files with pylint + cmds: + - task: scripts:lint:pylint + + e2e:list-workflows: + desc: List available E2E workflows + cmds: + - task: scripts:e2e:list-workflows + + e2e:run: + desc: Trigger selected E2E workflow via gh + cmds: + - task: scripts:e2e:run + + e2e:run:all: + desc: Trigger all E2E action workflows via gh + cmds: + - task: scripts:e2e:run:all + + e2e:view-latest: + desc: View latest run for selected E2E workflow + cmds: + - task: scripts:e2e:view-latest + + e2e:watch: + desc: Watch selected workflow run by RUN_ID + cmds: + - task: scripts:e2e:watch + + test:coverage:report: + desc: Report action input coverage from tests and E2E workflows + cmds: + - task: scripts:test:coverage:report + + test:coverage:gate: + desc: Fail on new uncovered action inputs not listed in baseline + cmds: + - task: scripts:test:coverage:gate + dependency:update: desc: 'No-op: no dedicated dependency updater configured for this profile' cmds: diff --git a/Taskfile.scripts.yml b/Taskfile.scripts.yml index f1cb976..627bf75 100644 --- a/Taskfile.scripts.yml +++ b/Taskfile.scripts.yml @@ -90,6 +90,119 @@ tasks: exit $rc fi + lint:pylint: + desc: Lint Python files with pylint + cmds: + - | + set -eu + echo "▶️ Running pylint..." + files="$(git ls-files '*.py' || true)" + if [ -z "${files}" ]; then + echo "ℹ️ No Python files found, skipping pylint" + exit 0 + fi + python3 -m pylint --rcfile .pylintrc ${files} + echo "✅ pylint passed" + + e2e:list-workflows: + desc: List E2E workflow files + shell: bash + cmds: + - | + set -eu + shopt -s nullglob + files=(.github/workflows/e2e-*.yml) + if [ "${#files[@]}" -eq 0 ]; then + echo "No E2E workflows found" + exit 0 + fi + printf '%s\n' "${files[@]}" | sort + + e2e:run: + desc: Run selected E2E workflow via gh + shell: bash + cmds: + - | + set -eu + workflow="${WORKFLOW:-}" + workflow_ref="${WORKFLOW_REF:-{{.GIT_BRANCH}}}" + if [ -z "$workflow" ]; then + echo "ERROR: set WORKFLOW, e.g. WORKFLOW=e2e-action-pull-request.yml" + exit 1 + fi + gh workflow run "$workflow" --repo "{{.GH_REPO}}" --ref "$workflow_ref" + echo "Triggered workflow '$workflow' in {{.GH_REPO}} on ref '$workflow_ref'" + + e2e:run:all: + desc: Trigger all E2E action workflows sequentially + shell: bash + cmds: + - | + set -eu + workflow_ref="${WORKFLOW_REF:-{{.GIT_BRANCH}}}" + shopt -s nullglob + files=(.github/workflows/e2e-action-*.yml) + if [ "${#files[@]}" -eq 0 ]; then + echo "No E2E action workflows found" + exit 0 + fi + mapfile -t workflows < <(printf '%s\n' "${files[@]}" | xargs -n1 basename | sort) + for workflow in "${workflows[@]}"; do + echo "Triggering ${workflow} in {{.GH_REPO}} on ref ${workflow_ref}" + gh workflow run "$workflow" --repo "{{.GH_REPO}}" --ref "$workflow_ref" + sleep 1 + done + echo "Triggered ${#workflows[@]} workflow(s) on ref '${workflow_ref}'." + + e2e:view-latest: + desc: Show latest run for selected E2E workflow + shell: bash + cmds: + - | + set -eu + workflow="${WORKFLOW:-}" + if [ -z "$workflow" ]; then + echo "ERROR: set WORKFLOW, e.g. WORKFLOW=e2e-action-pull-request.yml" + exit 1 + fi + gh run list --repo "{{.GH_REPO}}" --workflow "$workflow" --limit 1 + + e2e:watch: + desc: Watch a workflow run by RUN_ID + shell: bash + cmds: + - | + set -eu + run_id="${RUN_ID:-}" + if [ -z "$run_id" ]; then + echo "ERROR: set RUN_ID" + exit 1 + fi + gh run watch "$run_id" --repo "{{.GH_REPO}}" + + test:coverage:report: + desc: Report action input coverage from local tests and E2E workflows + shell: bash + cmds: + - | + set -eu + python3 scripts/check_action_input_coverage.py \ + --workspace-root /Users/christoph/IdeaProjects/devops-infra \ + --repo-root "$PWD" \ + --baseline-file "$PWD/tests/coverage-baseline.json" + + test:coverage:gate: + desc: Fail on new uncovered action inputs not in baseline + shell: bash + cmds: + - | + set -eu + python3 scripts/check_action_input_coverage.py \ + --workspace-root /Users/christoph/IdeaProjects/devops-infra \ + --repo-root "$PWD" \ + --baseline-file "$PWD/tests/coverage-baseline.json" \ + --strict + dependency:update: desc: 'No-op: no dedicated dependency updater configured for this profile' cmds: diff --git a/Taskfile.variables.yml b/Taskfile.variables.yml index b84a5ca..7fbb877 100644 --- a/Taskfile.variables.yml +++ b/Taskfile.variables.yml @@ -28,6 +28,13 @@ vars: fi PROJECT_DIR_NAME: sh: basename "$PWD" + GH_REPO: + sh: | + if [ -n "${GH_REPO:-}" ]; then + echo "${GH_REPO}" + else + gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null || echo "devops-infra/end-to-end-tests" + fi VERSION_OVERRIDE: sh: echo "${VERSION_OVERRIDE:-}" VERSION: diff --git a/scripts/check_action_input_coverage.py b/scripts/check_action_input_coverage.py new file mode 100644 index 0000000..f503828 --- /dev/null +++ b/scripts/check_action_input_coverage.py @@ -0,0 +1,224 @@ +"""Check action input coverage across local tests and E2E workflows.""" + +from __future__ import annotations + +import argparse +import json +import re +from pathlib import Path + + +TEXT_SUFFIXES = {".sh", ".py", ".yml", ".yaml", ".md", ".txt"} + + +def parse_action_inputs(action_file: Path) -> list[str]: + """Parse top-level `inputs` keys from an action.yml file.""" + lines = action_file.read_text(encoding="utf-8").splitlines() + inputs_indent: int | None = None + start = 0 + keys: list[str] = [] + + for idx, line in enumerate(lines): + if re.match(r"^\s*inputs:\s*$", line): + inputs_indent = len(line) - len(line.lstrip(" ")) + start = idx + 1 + break + + if inputs_indent is None: + return [] + + for line in lines[start:]: + stripped = line.strip() + if not stripped or line.lstrip().startswith("#"): + continue + + indent = len(line) - len(line.lstrip(" ")) + if indent <= inputs_indent: + break + + match = re.match(r"^(\s+)([A-Za-z0-9_]+):\s*$", line) + if not match: + continue + + key_indent = len(match.group(1)) + if key_indent == inputs_indent + 2: + keys.append(match.group(2)) + + return keys + + +def read_text_if_possible(path: Path) -> str: + """Read UTF-8 text from path, returning empty string on read errors.""" + if not path.exists() or not path.is_file(): + return "" + try: + return path.read_text(encoding="utf-8") + except OSError: + return "" + + +def input_is_covered(input_name: str, corpus: str) -> bool: + """Check whether an input name appears in test corpus directly or as INPUT_* env.""" + escaped = re.escape(input_name) + env_name = re.escape(f"INPUT_{input_name.upper()}") + pattern = re.compile(rf"\b{escaped}\b|\b{env_name}\b") + return bool(pattern.search(corpus)) + + +def collect_test_corpus(action_repo_dir: Path, e2e_workflow: Path) -> str: + """Collect all relevant textual test sources for coverage matching.""" + parts: list[str] = [] + if e2e_workflow.exists(): + parts.append(read_text_if_possible(e2e_workflow)) + + tests_dir = action_repo_dir / "tests" + if tests_dir.exists(): + for file_path in sorted(tests_dir.rglob("*")): + if file_path.is_file() and file_path.suffix in TEXT_SUFFIXES: + parts.append(read_text_if_possible(file_path)) + + return "\n".join(parts) + + +def load_baseline(path: Path) -> dict[str, list[str]]: + """Load optional JSON baseline mapping action repo to uncovered inputs.""" + if not path.exists(): + return {} + + raw = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(raw, dict): + return {} + + normalized: dict[str, list[str]] = {} + for key, value in raw.items(): + if isinstance(value, list): + normalized[key] = sorted(str(v) for v in value) + return normalized + + +def build_parser() -> argparse.ArgumentParser: + """Build command-line argument parser.""" + parser = argparse.ArgumentParser( + description="Check action input coverage in tests" + ) + parser.add_argument( + "--workspace-root", + required=True, + help="Workspace root containing action-* repos", + ) + parser.add_argument( + "--repo-root", + required=True, + help="Path to end-to-end-tests repository", + ) + parser.add_argument( + "--baseline-file", + required=True, + help="JSON baseline file for known gaps", + ) + parser.add_argument( + "--strict", + action="store_true", + help="Fail on gaps not present in baseline", + ) + parser.add_argument( + "--write-baseline", + action="store_true", + help="Write current uncovered gaps to baseline", + ) + return parser + + +def collect_results(workspace_root: Path, repo_root: Path) -> dict[str, list[str]]: + """Collect uncovered input names per action repository.""" + results: dict[str, list[str]] = {} + + for action_repo in sorted(workspace_root.glob("action-*")): + action_file = action_repo / "action.yml" + if not action_file.exists(): + continue + + action_name = action_repo.name + e2e_workflow = repo_root / ".github" / "workflows" / f"e2e-{action_name}.yml" + inputs = parse_action_inputs(action_file) + corpus = collect_test_corpus(action_repo, e2e_workflow) + uncovered = [ + input_name + for input_name in inputs + if not input_is_covered(input_name, corpus) + ] + results[action_name] = sorted(uncovered) + + return results + + +def write_baseline_file(results: dict[str, list[str]], baseline_file: Path) -> int: + """Write JSON baseline and exit successfully.""" + baseline_file.parent.mkdir(parents=True, exist_ok=True) + payload = json.dumps(results, indent=2, sort_keys=True) + "\n" + baseline_file.write_text(payload, encoding="utf-8") + print(f"Wrote baseline: {baseline_file}") + return 0 + + +def print_report(results: dict[str, list[str]]) -> None: + """Print human-readable coverage summary.""" + print("Action input coverage report") + for action_name, uncovered in results.items(): + if uncovered: + missing = ", ".join(uncovered) + print(f"- {action_name}: missing {len(uncovered)} -> {missing}") + else: + print(f"- {action_name}: fully covered") + + +def strict_gate(results: dict[str, list[str]], baseline_file: Path) -> int: + """Apply strict gate against baseline and return process code.""" + baseline = load_baseline(baseline_file) + has_new_gap = False + + for action_name, uncovered in results.items(): + allowed = set(baseline.get(action_name, [])) + new_gaps = sorted(set(uncovered) - allowed) + resolved = sorted(allowed - set(uncovered)) + + if new_gaps: + has_new_gap = True + joined = ", ".join(new_gaps) + print( + f"[ERROR] {action_name}: " + f"new uncovered inputs not in baseline -> {joined}" + ) + if resolved: + joined = ", ".join(resolved) + print( + f"[INFO] {action_name}: " + f"baseline can be tightened, now covered -> {joined}" + ) + + return 1 if has_new_gap else 0 + + +def main() -> int: + """Run coverage check command.""" + parser = build_parser() + args = parser.parse_args() + + workspace_root = Path(args.workspace_root).resolve() + repo_root = Path(args.repo_root).resolve() + baseline_file = Path(args.baseline_file).resolve() + + results = collect_results(workspace_root, repo_root) + + if args.write_baseline: + return write_baseline_file(results, baseline_file) + + print_report(results) + if not args.strict: + return 0 + + return strict_gate(results, baseline_file) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/coverage-baseline.json b/tests/coverage-baseline.json new file mode 100644 index 0000000..0b242e4 --- /dev/null +++ b/tests/coverage-baseline.json @@ -0,0 +1,9 @@ +{ + "action-commit-push": [], + "action-container-structure-test": [], + "action-format-hcl": [], + "action-pull-request": [], + "action-terraform-copy-vars": [], + "action-terraform-validate": [], + "action-tflint": [] +} diff --git a/triglav.jpeg b/triglav.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..d671fd240ea47ab84fe2439d7cea300e278fc3a1 GIT binary patch literal 145328 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ɂX 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 z%0A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%0DYykhf7<$tzTSS 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 SVvdNr1W~&i4x8W{fB)GsRP8zd literal 0 HcmV?d00001 From 3c79df16a442fed29df0a3b3d8d08155198966ff Mon Sep 17 00:00:00 2001 From: ChristophShyper <45788587+ChristophShyper@users.noreply.github.com> Date: Sun, 24 May 2026 14:03:43 +0200 Subject: [PATCH 05/15] refactor: update repository references to Triglav in documentation and scripts --- README.md | 18 +++++++++++++++--- Taskfile.variables.yml | 2 +- scripts/check_action_input_coverage.py | 2 +- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 04e62ef..84c09f5 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,19 @@ -# End-to-End Tests Framework +# Triglav + +## End-to-End Tests Framework Repository-level framework used to validate `devops-infra` automation end-to-end, with a focus on GitHub Actions behavior in real workflow runs. +![Triglav](triglav.jpeg) + +## Why Triglav + +In Slavic mythology, Triglav represents three realms. That maps well to this framework's validation layers: + +- pull request lifecycle and branch management behavior +- integration tests against live GitHub runtime +- periodic regression testing to catch unexpected changes + ## Scope - Executes reusable and action-specific E2E workflows in this repository. @@ -19,7 +31,7 @@ Repository-level framework used to validate `devops-infra` automation end-to-end | `devops-infra/action-terraform-copy-vars` | `.github/workflows/e2e-action-terraform-copy-vars.yml` | variable propagation across modules, custom path inputs, strict missing-variable failure mode | | `devops-infra/action-terraform-validate` | `.github/workflows/e2e-action-terraform-validate.yml` | valid module validation, scoped validation via `dir_filter` | | `devops-infra/action-tflint` | `.github/workflows/e2e-action-tflint.yml` | lint execution across modules, scoped lint via `dir_filter`, non-blocking findings mode | -| `devops-infra/template-action` | `.github/workflows/e2e-action-template-action.yml` | baseline template behavior validation, output contract checks, debug-mode execution | +| `devops-infra/template-action` | `.github/workflows/e2e-action-template-action.yml` | baseline template behavior validation, output contract checks, debug-mode execution | ## Workflow Orchestration @@ -114,7 +126,7 @@ Example caller from another action repository: ```yaml jobs: e2e-pr-validation: - uses: devops-infra/end-to-end-tests/.github/workflows/e2e-action-pull-request.yml@master + uses: devops-infra/triglav/.github/workflows/e2e-action-pull-request.yml@master with: mode: image image_tag: v1.2.3-test diff --git a/Taskfile.variables.yml b/Taskfile.variables.yml index 7fbb877..682dfa9 100644 --- a/Taskfile.variables.yml +++ b/Taskfile.variables.yml @@ -33,7 +33,7 @@ vars: if [ -n "${GH_REPO:-}" ]; then echo "${GH_REPO}" else - gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null || echo "devops-infra/end-to-end-tests" + gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null || echo "devops-infra/triglav" fi VERSION_OVERRIDE: sh: echo "${VERSION_OVERRIDE:-}" diff --git a/scripts/check_action_input_coverage.py b/scripts/check_action_input_coverage.py index f503828..d54e3c8 100644 --- a/scripts/check_action_input_coverage.py +++ b/scripts/check_action_input_coverage.py @@ -109,7 +109,7 @@ def build_parser() -> argparse.ArgumentParser: parser.add_argument( "--repo-root", required=True, - help="Path to end-to-end-tests repository", + help="Path to triglav repository", ) parser.add_argument( "--baseline-file", From f648bc54ad112ac5ee653e76ed169cbdf5f1cf79 Mon Sep 17 00:00:00 2001 From: ChristophShyper <45788587+ChristophShyper@users.noreply.github.com> Date: Sun, 24 May 2026 14:10:21 +0200 Subject: [PATCH 06/15] refactor: update repository references to Triglav in documentation and scripts --- Taskfile.scripts.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Taskfile.scripts.yml b/Taskfile.scripts.yml index 627bf75..45b5c32 100644 --- a/Taskfile.scripts.yml +++ b/Taskfile.scripts.yml @@ -101,6 +101,10 @@ tasks: echo "ℹ️ No Python files found, skipping pylint" exit 0 fi + if ! python3 -m pylint --version >/dev/null 2>&1; then + echo "ℹ️ pylint not found, installing locally..." + python3 -m pip install --user --quiet pylint==3.3.4 + fi python3 -m pylint --rcfile .pylintrc ${files} echo "✅ pylint passed" From dfd401fcfda7571c138229e68464a20f69eda551 Mon Sep 17 00:00:00 2001 From: ChristophShyper <45788587+ChristophShyper@users.noreply.github.com> Date: Sun, 24 May 2026 15:20:00 +0200 Subject: [PATCH 07/15] feat: add end-to-end testing workflows with mode validation and action reference updates --- .env.example | 2 +- .github/workflows/cron-e2e-tests.yml | 8 ++++ .github/workflows/e2e-action-commit-push.yml | 6 ++- .../e2e-action-container-structure-test.yml | 7 ++++ .github/workflows/e2e-action-pull-request.yml | 12 ++++-- README.md | 39 +++++++++++++++---- Taskfile.scripts.yml | 6 ++- 7 files changed, 64 insertions(+), 16 deletions(-) diff --git a/.env.example b/.env.example index e72fdfe..7595e15 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,2 @@ GITHUB_TOKEN=ghp_your_token_here -GH_REPO=devops-infra/end-to-end-tests +GH_REPO=devops-infra/triglav diff --git a/.github/workflows/cron-e2e-tests.yml b/.github/workflows/cron-e2e-tests.yml index 281d329..a90b250 100644 --- a/.github/workflows/cron-e2e-tests.yml +++ b/.github/workflows/cron-e2e-tests.yml @@ -85,3 +85,11 @@ jobs: action_ref: ${{ github.event_name == 'workflow_dispatch' && inputs.action_ref || 'v1' }} image_tag: ${{ github.event_name == 'workflow_dispatch' && inputs.image_tag || '' }} secrets: inherit + + action-template-action: + uses: ./.github/workflows/e2e-action-template-action.yml + with: + mode: ${{ github.event_name == 'workflow_dispatch' && inputs.mode || 'ref' }} + action_ref: ${{ github.event_name == 'workflow_dispatch' && inputs.action_ref || 'v1' }} + image_tag: ${{ github.event_name == 'workflow_dispatch' && inputs.image_tag || '' }} + secrets: inherit diff --git a/.github/workflows/e2e-action-commit-push.yml b/.github/workflows/e2e-action-commit-push.yml index 40d2e1e..444e2bb 100644 --- a/.github/workflows/e2e-action-commit-push.yml +++ b/.github/workflows/e2e-action-commit-push.yml @@ -56,6 +56,7 @@ jobs: echo "Use ref mode for authoritative validation until image-mode harness is finalized." - name: Verify outputs + if: ${{ inputs.mode == 'ref' }} run: | echo "branch_name=${{ steps.commit.outputs.branch_name }}" echo "files_changed=${{ steps.commit.outputs.files_changed }}" @@ -99,6 +100,7 @@ jobs: echo "Use ref mode for authoritative validation until image-mode harness is finalized." - name: Verify outputs + if: ${{ inputs.mode == 'ref' }} run: | echo "branch_name=${{ steps.commit.outputs.branch_name }}" echo "files_changed=${{ steps.commit.outputs.files_changed }}" @@ -139,6 +141,7 @@ jobs: echo "Use ref mode for authoritative validation until image-mode harness is finalized." - name: Verify branch output + if: ${{ inputs.mode == 'ref' }} run: | echo "branch_name=${{ steps.commit.outputs.branch_name }}" test -n "${{ steps.commit.outputs.branch_name }}" @@ -187,7 +190,7 @@ jobs: [[ "${branch_name}" =~ ^test/e2e-commit-push-timestamp-[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}-[0-9]{2}-[0-9]{2}Z$ ]] - name: Cleanup - delete test branch - if: always() + if: ${{ always() && inputs.mode == 'ref' }} run: | branch_name="${{ steps.commit.outputs.branch_name }}" if [ -n "${branch_name}" ]; then @@ -415,6 +418,7 @@ jobs: echo "Use ref mode for authoritative validation until image-mode harness is finalized." - name: Verify branch output + if: ${{ inputs.mode == 'ref' }} run: | echo "branch_name=${{ steps.amend.outputs.branch_name }}" test -n "${{ steps.amend.outputs.branch_name }}" diff --git a/.github/workflows/e2e-action-container-structure-test.yml b/.github/workflows/e2e-action-container-structure-test.yml index 8933750..74fc368 100644 --- a/.github/workflows/e2e-action-container-structure-test.yml +++ b/.github/workflows/e2e-action-container-structure-test.yml @@ -54,6 +54,7 @@ jobs: echo "Use ref mode for authoritative validation until image-mode harness is finalized." - name: Verify test result outputs + if: ${{ inputs.mode == 'ref' }} run: | echo "total=${{ steps.cst.outputs.total }}" echo "passed=${{ steps.cst.outputs.passed }}" @@ -92,6 +93,7 @@ jobs: echo "Use ref mode for authoritative validation until image-mode harness is finalized." - name: Verify test result outputs + if: ${{ inputs.mode == 'ref' }} run: | echo "total=${{ steps.cst.outputs.total }}" echo "passed=${{ steps.cst.outputs.passed }}" @@ -129,6 +131,7 @@ jobs: echo "Use ref mode for authoritative validation until image-mode harness is finalized." - name: Verify test result outputs + if: ${{ inputs.mode == 'ref' }} run: | echo "total=${{ steps.cst.outputs.total }}" echo "passed=${{ steps.cst.outputs.passed }}" @@ -166,9 +169,11 @@ jobs: echo "Use ref mode for authoritative validation until image-mode harness is finalized." - name: Verify report file was created + if: ${{ inputs.mode == 'ref' }} run: test -f /tmp/cst-report.json - name: Verify test result outputs + if: ${{ inputs.mode == 'ref' }} run: | echo "total=${{ steps.cst.outputs.total }}" echo "passed=${{ steps.cst.outputs.passed }}" @@ -207,6 +212,7 @@ jobs: echo "Use ref mode for authoritative validation until image-mode harness is finalized." - name: Verify combined test results + if: ${{ inputs.mode == 'ref' }} run: | echo "total=${{ steps.cst.outputs.total }}" echo "passed=${{ steps.cst.outputs.passed }}" @@ -259,6 +265,7 @@ jobs: echo "Use ref mode for authoritative validation until image-mode harness is finalized." - name: Verify test result outputs + if: ${{ inputs.mode == 'ref' }} run: | echo "total=${{ steps.cst.outputs.total }}" echo "passed=${{ steps.cst.outputs.passed }}" diff --git a/.github/workflows/e2e-action-pull-request.yml b/.github/workflows/e2e-action-pull-request.yml index 40cd745..8d51916 100644 --- a/.github/workflows/e2e-action-pull-request.yml +++ b/.github/workflows/e2e-action-pull-request.yml @@ -66,6 +66,7 @@ jobs: echo "Use ref mode for authoritative validation until image-mode harness is finalized." - name: Verify outputs + if: ${{ inputs.mode == 'ref' }} run: | echo "url=${{ steps.pr.outputs.url }}" echo "pr_number=${{ steps.pr.outputs.pr_number }}" @@ -73,7 +74,7 @@ jobs: test -n "${{ steps.pr.outputs.pr_number }}" - name: Cleanup - close PR and delete test branch - if: always() + if: ${{ always() && inputs.mode == 'ref' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | @@ -127,6 +128,7 @@ jobs: echo "Use ref mode for authoritative validation until image-mode harness is finalized." - name: Verify outputs + if: ${{ inputs.mode == 'ref' }} run: | echo "url=${{ steps.pr.outputs.url }}" echo "pr_number=${{ steps.pr.outputs.pr_number }}" @@ -134,7 +136,7 @@ jobs: test -n "${{ steps.pr.outputs.pr_number }}" - name: Cleanup - close PR and delete test branch - if: always() + if: ${{ always() && inputs.mode == 'ref' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | @@ -188,6 +190,7 @@ jobs: echo "Use ref mode for authoritative validation until image-mode harness is finalized." - name: Verify outputs + if: ${{ inputs.mode == 'ref' }} run: | echo "url=${{ steps.pr.outputs.url }}" echo "pr_number=${{ steps.pr.outputs.pr_number }}" @@ -195,7 +198,7 @@ jobs: test -n "${{ steps.pr.outputs.pr_number }}" - name: Cleanup - close PR and delete test branch - if: always() + if: ${{ always() && inputs.mode == 'ref' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | @@ -253,6 +256,7 @@ jobs: echo "Use ref mode for authoritative validation until image-mode harness is finalized." - name: Verify outputs + if: ${{ inputs.mode == 'ref' }} run: | echo "url=${{ steps.pr.outputs.url }}" echo "pr_number=${{ steps.pr.outputs.pr_number }}" @@ -260,7 +264,7 @@ jobs: test -n "${{ steps.pr.outputs.pr_number }}" - name: Cleanup - close PR and delete test branch - if: always() + if: ${{ always() && inputs.mode == 'ref' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | diff --git a/README.md b/README.md index 84c09f5..b487ac5 100644 --- a/README.md +++ b/README.md @@ -112,21 +112,44 @@ Recommended pre-merge strategy: - Run E2E with action refs that point to the PR under test (branch or SHA). - Run image-tag verification stages for `-test` and `-rc` tags after image publication in release pipelines. -Current image-mode implementation: +Execution modes: -- `e2e-action-format-hcl.yml` supports executable `mode: image` using `image_tag`. -- `e2e-action-tflint.yml` supports executable `mode: image` using `image_tag`. -- `e2e-action-terraform-validate.yml` supports executable `mode: image` using `image_tag`. -- `e2e-action-terraform-copy-vars.yml` supports executable `mode: image` using `image_tag`. -- `e2e-action-container-structure-test.yml` currently uses `mode: ref` as authoritative path in reusable CI flows. -- `e2e-action-commit-push.yml` and `e2e-action-pull-request.yml` use `mode: ref` as authoritative path in reusable CI flows. +- `mode=ref` validates an action repository ref and requires `action_ref` (branch, tag, or SHA). This is authoritative for branch/SHA validation. +- `mode=image` validates a published Docker image and requires `image_tag`. This is authoritative in release image checks. +- Use immutable values in automation: commit SHA or release tag for `action_ref`, and semantic tags (`vX.Y.Z-test`, `vX.Y.Z-rc`, `vX.Y.Z`) for `image_tag`. +- Do not use `latest-test` for `action_ref`; `latest-test` is an image tag, not a Git ref. -Example caller from another action repository: +`action_ref` implementation note: + +- GitHub Actions does not allow expressions in local step `uses:` references, so `uses: org/repo@${{ ... }}` is invalid in these E2E workflow steps. +- Dynamic `action_ref` selection is therefore enforced at the caller/orchestration level, while workflow steps use stable pinned refs. + +Current mode behavior by workflow: + +- `e2e-action-commit-push.yml`: `ref` authoritative, `image` placeholder preview. +- `e2e-action-pull-request.yml`: `ref` authoritative, `image` placeholder preview. +- `e2e-action-template-action.yml`: `ref` authoritative, `image` placeholder preview. +- `e2e-action-container-structure-test.yml`: `ref` authoritative in reusable CI flows, `image` preview. +- `e2e-action-format-hcl.yml`: `ref` and executable `image` supported. +- `e2e-action-tflint.yml`: `ref` and executable `image` supported. +- `e2e-action-terraform-validate.yml`: `ref` and executable `image` supported. +- `e2e-action-terraform-copy-vars.yml`: `ref` and executable `image` supported. + +Example callers from another action repository: ```yaml jobs: e2e-pr-validation: uses: devops-infra/triglav/.github/workflows/e2e-action-pull-request.yml@master + with: + mode: ref + action_ref: ${{ github.sha }} +``` + +```yaml +jobs: + e2e-image-validation: + uses: devops-infra/triglav/.github/workflows/e2e-action-format-hcl.yml@master with: mode: image image_tag: v1.2.3-test diff --git a/Taskfile.scripts.yml b/Taskfile.scripts.yml index 45b5c32..af59087 100644 --- a/Taskfile.scripts.yml +++ b/Taskfile.scripts.yml @@ -190,8 +190,9 @@ tasks: cmds: - | set -eu + workspace_root="${WORKSPACE_ROOT:-$(dirname "$PWD")}" python3 scripts/check_action_input_coverage.py \ - --workspace-root /Users/christoph/IdeaProjects/devops-infra \ + --workspace-root "$workspace_root" \ --repo-root "$PWD" \ --baseline-file "$PWD/tests/coverage-baseline.json" @@ -201,8 +202,9 @@ tasks: cmds: - | set -eu + workspace_root="${WORKSPACE_ROOT:-$(dirname "$PWD")}" python3 scripts/check_action_input_coverage.py \ - --workspace-root /Users/christoph/IdeaProjects/devops-infra \ + --workspace-root "$workspace_root" \ --repo-root "$PWD" \ --baseline-file "$PWD/tests/coverage-baseline.json" \ --strict From a608646e7907672e5e6cf96682e5d2a1dd5e83cd Mon Sep 17 00:00:00 2001 From: ChristophShyper <45788587+ChristophShyper@users.noreply.github.com> Date: Sun, 24 May 2026 15:52:47 +0200 Subject: [PATCH 08/15] feat: add end-to-end testing workflows and refine input handling --- .env.example | 2 +- .github/workflows/cron-e2e-tests.yml | 35 ++++++++++--------- .github/workflows/e2e-action-commit-push.yml | 5 --- .../e2e-action-container-structure-test.yml | 5 --- .github/workflows/e2e-action-format-hcl.yml | 15 +++++--- .github/workflows/e2e-action-pull-request.yml | 5 --- .../workflows/e2e-action-template-action.yml | 5 --- .../e2e-action-terraform-copy-vars.yml | 5 --- .../e2e-action-terraform-validate.yml | 5 --- .github/workflows/e2e-action-tflint.yml | 5 --- .pre-commit-config.yaml | 1 + README.md | 12 ++----- scripts/check_action_input_coverage.py | 2 +- 13 files changed, 35 insertions(+), 67 deletions(-) diff --git a/.env.example b/.env.example index 7595e15..278e071 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,2 @@ -GITHUB_TOKEN=ghp_your_token_here +GITHUB_TOKEN=your_github_token_here GH_REPO=devops-infra/triglav diff --git a/.github/workflows/cron-e2e-tests.yml b/.github/workflows/cron-e2e-tests.yml index a90b250..75d98bd 100644 --- a/.github/workflows/cron-e2e-tests.yml +++ b/.github/workflows/cron-e2e-tests.yml @@ -13,11 +13,6 @@ on: options: - ref - image - action_ref: - description: Action git ref used when mode=ref - required: false - default: v1 - type: string image_tag: description: Action image tag used when mode=image (for example v1.2.3-test or v1.2.3-rc) required: false @@ -25,71 +20,79 @@ on: type: string permissions: - contents: write - pull-requests: write - issues: write + contents: read jobs: action-commit-push: + permissions: + contents: write uses: ./.github/workflows/e2e-action-commit-push.yml with: mode: ${{ github.event_name == 'workflow_dispatch' && inputs.mode || 'ref' }} - action_ref: ${{ github.event_name == 'workflow_dispatch' && inputs.action_ref || 'v1' }} image_tag: ${{ github.event_name == 'workflow_dispatch' && inputs.image_tag || '' }} secrets: inherit action-pull-request: + permissions: + contents: write + pull-requests: write + issues: write uses: ./.github/workflows/e2e-action-pull-request.yml with: mode: ${{ github.event_name == 'workflow_dispatch' && inputs.mode || 'ref' }} - action_ref: ${{ github.event_name == 'workflow_dispatch' && inputs.action_ref || 'v1' }} image_tag: ${{ github.event_name == 'workflow_dispatch' && inputs.image_tag || '' }} secrets: inherit action-tflint: + permissions: + contents: read uses: ./.github/workflows/e2e-action-tflint.yml with: mode: ${{ github.event_name == 'workflow_dispatch' && inputs.mode || 'ref' }} - action_ref: ${{ github.event_name == 'workflow_dispatch' && inputs.action_ref || 'v1' }} image_tag: ${{ github.event_name == 'workflow_dispatch' && inputs.image_tag || '' }} secrets: inherit action-format-hcl: + permissions: + contents: read uses: ./.github/workflows/e2e-action-format-hcl.yml with: mode: ${{ github.event_name == 'workflow_dispatch' && inputs.mode || 'ref' }} - action_ref: ${{ github.event_name == 'workflow_dispatch' && inputs.action_ref || 'v1' }} image_tag: ${{ github.event_name == 'workflow_dispatch' && inputs.image_tag || '' }} secrets: inherit action-terraform-validate: + permissions: + contents: read uses: ./.github/workflows/e2e-action-terraform-validate.yml with: mode: ${{ github.event_name == 'workflow_dispatch' && inputs.mode || 'ref' }} - action_ref: ${{ github.event_name == 'workflow_dispatch' && inputs.action_ref || 'v1' }} image_tag: ${{ github.event_name == 'workflow_dispatch' && inputs.image_tag || '' }} secrets: inherit action-terraform-copy-vars: + permissions: + contents: read uses: ./.github/workflows/e2e-action-terraform-copy-vars.yml with: mode: ${{ github.event_name == 'workflow_dispatch' && inputs.mode || 'ref' }} - action_ref: ${{ github.event_name == 'workflow_dispatch' && inputs.action_ref || 'v1' }} image_tag: ${{ github.event_name == 'workflow_dispatch' && inputs.image_tag || '' }} secrets: inherit action-container-structure-test: + permissions: + contents: read uses: ./.github/workflows/e2e-action-container-structure-test.yml with: mode: ${{ github.event_name == 'workflow_dispatch' && inputs.mode || 'ref' }} - action_ref: ${{ github.event_name == 'workflow_dispatch' && inputs.action_ref || 'v1' }} image_tag: ${{ github.event_name == 'workflow_dispatch' && inputs.image_tag || '' }} secrets: inherit action-template-action: + permissions: + contents: read uses: ./.github/workflows/e2e-action-template-action.yml with: mode: ${{ github.event_name == 'workflow_dispatch' && inputs.mode || 'ref' }} - action_ref: ${{ github.event_name == 'workflow_dispatch' && inputs.action_ref || 'v1' }} image_tag: ${{ github.event_name == 'workflow_dispatch' && inputs.image_tag || '' }} secrets: inherit diff --git a/.github/workflows/e2e-action-commit-push.yml b/.github/workflows/e2e-action-commit-push.yml index 444e2bb..41c7bf5 100644 --- a/.github/workflows/e2e-action-commit-push.yml +++ b/.github/workflows/e2e-action-commit-push.yml @@ -3,11 +3,6 @@ name: (E2E) Action Commit Push on: workflow_call: inputs: - action_ref: - description: Git ref of devops-infra/action-commit-push to validate - required: false - type: string - default: v1 mode: description: Execution mode (ref or image) required: false diff --git a/.github/workflows/e2e-action-container-structure-test.yml b/.github/workflows/e2e-action-container-structure-test.yml index 74fc368..fc9a60c 100644 --- a/.github/workflows/e2e-action-container-structure-test.yml +++ b/.github/workflows/e2e-action-container-structure-test.yml @@ -3,11 +3,6 @@ name: (E2E) Action Container Structure Test on: workflow_call: inputs: - action_ref: - description: Git ref of devops-infra/action-container-structure-test to validate - required: false - type: string - default: v1 mode: description: Execution mode (ref or image) required: false diff --git a/.github/workflows/e2e-action-format-hcl.yml b/.github/workflows/e2e-action-format-hcl.yml index b389549..d43b9b7 100644 --- a/.github/workflows/e2e-action-format-hcl.yml +++ b/.github/workflows/e2e-action-format-hcl.yml @@ -3,11 +3,6 @@ name: (E2E) Action Format HCL on: workflow_call: inputs: - action_ref: - description: Git ref of devops-infra/action-format-hcl to validate - required: false - type: string - default: v1 mode: description: Execution mode (ref or image) required: false @@ -28,6 +23,16 @@ jobs: name: Check mode on already-formatted files passes runs-on: ubuntu-latest steps: + - name: Validate mode input + run: | + case "${{ inputs.mode }}" in + ref|image) ;; + *) + echo "Unsupported mode '${{ inputs.mode }}'. Expected 'ref' or 'image'." + exit 1 + ;; + esac + - name: Checkout repository uses: actions/checkout@v6 diff --git a/.github/workflows/e2e-action-pull-request.yml b/.github/workflows/e2e-action-pull-request.yml index 8d51916..1430315 100644 --- a/.github/workflows/e2e-action-pull-request.yml +++ b/.github/workflows/e2e-action-pull-request.yml @@ -3,11 +3,6 @@ name: (E2E) Action Pull Request on: workflow_call: inputs: - action_ref: - description: Git ref of devops-infra/action-pull-request to validate - required: false - type: string - default: v1 mode: description: Execution mode (ref or image) required: false diff --git a/.github/workflows/e2e-action-template-action.yml b/.github/workflows/e2e-action-template-action.yml index f53356d..b014f82 100644 --- a/.github/workflows/e2e-action-template-action.yml +++ b/.github/workflows/e2e-action-template-action.yml @@ -3,11 +3,6 @@ name: (E2E) Template Action on: workflow_call: inputs: - action_ref: - description: Git ref of devops-infra/template-action to validate - required: false - type: string - default: v1 mode: description: Execution mode (ref or image) required: false diff --git a/.github/workflows/e2e-action-terraform-copy-vars.yml b/.github/workflows/e2e-action-terraform-copy-vars.yml index dfb1916..f8c27aa 100644 --- a/.github/workflows/e2e-action-terraform-copy-vars.yml +++ b/.github/workflows/e2e-action-terraform-copy-vars.yml @@ -3,11 +3,6 @@ name: (E2E) Action Terraform Copy Vars on: workflow_call: inputs: - action_ref: - description: Git ref of devops-infra/action-terraform-copy-vars to validate - required: false - type: string - default: v1 mode: description: Execution mode (ref or image) required: false diff --git a/.github/workflows/e2e-action-terraform-validate.yml b/.github/workflows/e2e-action-terraform-validate.yml index 970aa43..2fb9425 100644 --- a/.github/workflows/e2e-action-terraform-validate.yml +++ b/.github/workflows/e2e-action-terraform-validate.yml @@ -3,11 +3,6 @@ name: (E2E) Action Terraform Validate on: workflow_call: inputs: - action_ref: - description: Git ref of devops-infra/action-terraform-validate to validate - required: false - type: string - default: v1 mode: description: Execution mode (ref or image) required: false diff --git a/.github/workflows/e2e-action-tflint.yml b/.github/workflows/e2e-action-tflint.yml index bebb3a1..5ad4c69 100644 --- a/.github/workflows/e2e-action-tflint.yml +++ b/.github/workflows/e2e-action-tflint.yml @@ -3,11 +3,6 @@ name: (E2E) Action TFLint on: workflow_call: inputs: - action_ref: - description: Git ref of devops-infra/action-tflint to validate - required: false - type: string - default: v1 mode: description: Execution mode (ref or image) required: false diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1cb5dfb..02e7314 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -43,4 +43,5 @@ repos: name: pylint entry: python3 -m pylint --rcfile .pylintrc language: system + # Keep language: system to align with this repository's Docker/system-based hooks. files: \.py$ diff --git a/README.md b/README.md index b487ac5..5ee6ab7 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ Prerequisites: - `task` - `docker` - `gh` (authenticated) +- `python3 -m pylint` available in your environment (local install is acceptable) Common commands: @@ -114,15 +115,9 @@ Recommended pre-merge strategy: Execution modes: -- `mode=ref` validates an action repository ref and requires `action_ref` (branch, tag, or SHA). This is authoritative for branch/SHA validation. +- `mode=ref` runs ref-oriented E2E paths against stable pinned action refs. - `mode=image` validates a published Docker image and requires `image_tag`. This is authoritative in release image checks. -- Use immutable values in automation: commit SHA or release tag for `action_ref`, and semantic tags (`vX.Y.Z-test`, `vX.Y.Z-rc`, `vX.Y.Z`) for `image_tag`. -- Do not use `latest-test` for `action_ref`; `latest-test` is an image tag, not a Git ref. - -`action_ref` implementation note: - -- GitHub Actions does not allow expressions in local step `uses:` references, so `uses: org/repo@${{ ... }}` is invalid in these E2E workflow steps. -- Dynamic `action_ref` selection is therefore enforced at the caller/orchestration level, while workflow steps use stable pinned refs. +- Use semantic tags for `image_tag` in automation (`vX.Y.Z-test`, `vX.Y.Z-rc`, `vX.Y.Z`) instead of mutable aliases. Current mode behavior by workflow: @@ -143,7 +138,6 @@ jobs: uses: devops-infra/triglav/.github/workflows/e2e-action-pull-request.yml@master with: mode: ref - action_ref: ${{ github.sha }} ``` ```yaml diff --git a/scripts/check_action_input_coverage.py b/scripts/check_action_input_coverage.py index d54e3c8..9167c6b 100644 --- a/scripts/check_action_input_coverage.py +++ b/scripts/check_action_input_coverage.py @@ -53,7 +53,7 @@ def read_text_if_possible(path: Path) -> str: return "" try: return path.read_text(encoding="utf-8") - except OSError: + except (OSError, UnicodeDecodeError): return "" From d53d46d9c49564da4cfcc6a5b79f904105ba52d1 Mon Sep 17 00:00:00 2001 From: ChristophShyper <45788587+ChristophShyper@users.noreply.github.com> Date: Sun, 24 May 2026 16:18:52 +0200 Subject: [PATCH 09/15] feat: add execution mode and image tag inputs to workflow dispatch for E2E actions --- .github/workflows/e2e-action-commit-push.yml | 14 ++++++++++++++ .../e2e-action-container-structure-test.yml | 14 ++++++++++++++ .github/workflows/e2e-action-format-hcl.yml | 14 ++++++++++++++ .github/workflows/e2e-action-pull-request.yml | 14 ++++++++++++++ .../workflows/e2e-action-template-action.yml | 14 ++++++++++++++ .../e2e-action-terraform-copy-vars.yml | 14 ++++++++++++++ .../workflows/e2e-action-terraform-validate.yml | 14 ++++++++++++++ .github/workflows/e2e-action-tflint.yml | 14 ++++++++++++++ README.md | 3 +++ Taskfile.scripts.yml | 17 ++++++++++++----- 10 files changed, 127 insertions(+), 5 deletions(-) diff --git a/.github/workflows/e2e-action-commit-push.yml b/.github/workflows/e2e-action-commit-push.yml index 41c7bf5..c17fc0d 100644 --- a/.github/workflows/e2e-action-commit-push.yml +++ b/.github/workflows/e2e-action-commit-push.yml @@ -14,6 +14,20 @@ on: type: string default: '' workflow_dispatch: + inputs: + mode: + description: Execution mode (ref or image) + required: false + default: ref + type: choice + options: + - ref + - image + image_tag: + description: Image tag for docker mode (for example v1.2.3-test or v1.2.3-rc) + required: false + default: '' + type: string permissions: contents: write diff --git a/.github/workflows/e2e-action-container-structure-test.yml b/.github/workflows/e2e-action-container-structure-test.yml index fc9a60c..d6b50dd 100644 --- a/.github/workflows/e2e-action-container-structure-test.yml +++ b/.github/workflows/e2e-action-container-structure-test.yml @@ -14,6 +14,20 @@ on: type: string default: '' workflow_dispatch: + inputs: + mode: + description: Execution mode (ref or image) + required: false + default: ref + type: choice + options: + - ref + - image + image_tag: + description: Image tag for docker mode (for example v1.2.3-test or v1.2.3-rc) + required: false + default: '' + type: string permissions: contents: read diff --git a/.github/workflows/e2e-action-format-hcl.yml b/.github/workflows/e2e-action-format-hcl.yml index d43b9b7..593fcbb 100644 --- a/.github/workflows/e2e-action-format-hcl.yml +++ b/.github/workflows/e2e-action-format-hcl.yml @@ -14,6 +14,20 @@ on: type: string default: '' workflow_dispatch: + inputs: + mode: + description: Execution mode (ref or image) + required: false + default: ref + type: choice + options: + - ref + - image + image_tag: + description: Image tag for docker mode (for example v1.2.3-test or v1.2.3-rc) + required: false + default: '' + type: string permissions: contents: read diff --git a/.github/workflows/e2e-action-pull-request.yml b/.github/workflows/e2e-action-pull-request.yml index 1430315..754fcab 100644 --- a/.github/workflows/e2e-action-pull-request.yml +++ b/.github/workflows/e2e-action-pull-request.yml @@ -14,6 +14,20 @@ on: type: string default: '' workflow_dispatch: + inputs: + mode: + description: Execution mode (ref or image) + required: false + default: ref + type: choice + options: + - ref + - image + image_tag: + description: Image tag for docker mode (for example v1.2.3-test or v1.2.3-rc) + required: false + default: '' + type: string permissions: contents: write diff --git a/.github/workflows/e2e-action-template-action.yml b/.github/workflows/e2e-action-template-action.yml index b014f82..13f4b6f 100644 --- a/.github/workflows/e2e-action-template-action.yml +++ b/.github/workflows/e2e-action-template-action.yml @@ -14,6 +14,20 @@ on: type: string default: '' workflow_dispatch: + inputs: + mode: + description: Execution mode (ref or image) + required: false + default: ref + type: choice + options: + - ref + - image + image_tag: + description: Image tag for docker mode (for example v1.2.3-test or v1.2.3-rc) + required: false + default: '' + type: string permissions: contents: read diff --git a/.github/workflows/e2e-action-terraform-copy-vars.yml b/.github/workflows/e2e-action-terraform-copy-vars.yml index f8c27aa..a7c27a8 100644 --- a/.github/workflows/e2e-action-terraform-copy-vars.yml +++ b/.github/workflows/e2e-action-terraform-copy-vars.yml @@ -14,6 +14,20 @@ on: type: string default: '' workflow_dispatch: + inputs: + mode: + description: Execution mode (ref or image) + required: false + default: ref + type: choice + options: + - ref + - image + image_tag: + description: Image tag for docker mode (for example v1.2.3-test or v1.2.3-rc) + required: false + default: '' + type: string permissions: contents: read diff --git a/.github/workflows/e2e-action-terraform-validate.yml b/.github/workflows/e2e-action-terraform-validate.yml index 2fb9425..7bf0511 100644 --- a/.github/workflows/e2e-action-terraform-validate.yml +++ b/.github/workflows/e2e-action-terraform-validate.yml @@ -14,6 +14,20 @@ on: type: string default: '' workflow_dispatch: + inputs: + mode: + description: Execution mode (ref or image) + required: false + default: ref + type: choice + options: + - ref + - image + image_tag: + description: Image tag for docker mode (for example v1.2.3-test or v1.2.3-rc) + required: false + default: '' + type: string permissions: contents: read diff --git a/.github/workflows/e2e-action-tflint.yml b/.github/workflows/e2e-action-tflint.yml index 5ad4c69..b8a051e 100644 --- a/.github/workflows/e2e-action-tflint.yml +++ b/.github/workflows/e2e-action-tflint.yml @@ -14,6 +14,20 @@ on: type: string default: '' workflow_dispatch: + inputs: + mode: + description: Execution mode (ref or image) + required: false + default: ref + type: choice + options: + - ref + - image + image_tag: + description: Image tag for docker mode (for example v1.2.3-test or v1.2.3-rc) + required: false + default: '' + type: string permissions: contents: read diff --git a/README.md b/README.md index 5ee6ab7..8fe2150 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,9 @@ task lint task pre-commit task e2e:list-workflows task e2e:run WORKFLOW=e2e-action-pull-request.yml +task e2e:run WORKFLOW=e2e-action-format-hcl.yml MODE=image IMAGE_TAG=v1.2.3-test task e2e:run:all +task e2e:run:all MODE=image IMAGE_TAG=v1.2.3-test task test:coverage:report task test:coverage:gate ``` @@ -85,6 +87,7 @@ Manual dispatch examples: ```bash task e2e:run WORKFLOW=e2e-action-commit-push.yml task e2e:run:all +task e2e:run WORKFLOW=e2e-action-tflint.yml MODE=image IMAGE_TAG=v1.2.3-test ``` This repository primarily relies on the built-in `${{ secrets.GITHUB_TOKEN }}` in workflow runs. diff --git a/Taskfile.scripts.yml b/Taskfile.scripts.yml index af59087..8d788f3 100644 --- a/Taskfile.scripts.yml +++ b/Taskfile.scripts.yml @@ -92,12 +92,15 @@ tasks: lint:pylint: desc: Lint Python files with pylint + shell: bash cmds: - | set -eu echo "▶️ Running pylint..." - files="$(git ls-files '*.py' || true)" - if [ -z "${files}" ]; then + tmp_files="$(mktemp)" + trap 'rm -f "$tmp_files"' EXIT + git ls-files -z '*.py' > "$tmp_files" + if [ ! -s "$tmp_files" ]; then echo "ℹ️ No Python files found, skipping pylint" exit 0 fi @@ -105,7 +108,7 @@ tasks: echo "ℹ️ pylint not found, installing locally..." python3 -m pip install --user --quiet pylint==3.3.4 fi - python3 -m pylint --rcfile .pylintrc ${files} + xargs -0 python3 -m pylint --rcfile .pylintrc < "$tmp_files" echo "✅ pylint passed" e2e:list-workflows: @@ -134,7 +137,9 @@ tasks: echo "ERROR: set WORKFLOW, e.g. WORKFLOW=e2e-action-pull-request.yml" exit 1 fi - gh workflow run "$workflow" --repo "{{.GH_REPO}}" --ref "$workflow_ref" + mode="${MODE:-ref}" + image_tag="${IMAGE_TAG:-}" + gh workflow run "$workflow" --repo "{{.GH_REPO}}" --ref "$workflow_ref" -f mode="$mode" -f image_tag="$image_tag" echo "Triggered workflow '$workflow' in {{.GH_REPO}} on ref '$workflow_ref'" e2e:run:all: @@ -151,9 +156,11 @@ tasks: exit 0 fi mapfile -t workflows < <(printf '%s\n' "${files[@]}" | xargs -n1 basename | sort) + mode="${MODE:-ref}" + image_tag="${IMAGE_TAG:-}" for workflow in "${workflows[@]}"; do echo "Triggering ${workflow} in {{.GH_REPO}} on ref ${workflow_ref}" - gh workflow run "$workflow" --repo "{{.GH_REPO}}" --ref "$workflow_ref" + gh workflow run "$workflow" --repo "{{.GH_REPO}}" --ref "$workflow_ref" -f mode="$mode" -f image_tag="$image_tag" sleep 1 done echo "Triggered ${#workflows[@]} workflow(s) on ref '${workflow_ref}'." From 59c02c9d6fcf7ea1ba8b8652b2f9a762fc4220b4 Mon Sep 17 00:00:00 2001 From: ChristophShyper <45788587+ChristophShyper@users.noreply.github.com> Date: Sun, 24 May 2026 16:36:23 +0200 Subject: [PATCH 10/15] feat: enhance action input parsing and add unit tests for coverage --- .github/workflows/e2e-action-pull-request.yml | 4 ++ scripts/check_action_input_coverage.py | 31 +++++++-- tests/test_check_action_input_coverage.py | 68 +++++++++++++++++++ 3 files changed, 98 insertions(+), 5 deletions(-) create mode 100644 tests/test_check_action_input_coverage.py diff --git a/.github/workflows/e2e-action-pull-request.yml b/.github/workflows/e2e-action-pull-request.yml index 754fcab..20a535e 100644 --- a/.github/workflows/e2e-action-pull-request.yml +++ b/.github/workflows/e2e-action-pull-request.yml @@ -45,6 +45,7 @@ jobs: fetch-depth: 0 - name: Create test branch with a commit + if: ${{ inputs.mode == 'ref' }} run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" @@ -103,6 +104,7 @@ jobs: fetch-depth: 0 - name: Create test branch with a commit + if: ${{ inputs.mode == 'ref' }} run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" @@ -166,6 +168,7 @@ jobs: path: work/repo - name: Create test branch with a commit inside custom path + if: ${{ inputs.mode == 'ref' }} run: | cd work/repo git config user.name "github-actions[bot]" @@ -228,6 +231,7 @@ jobs: fetch-depth: 0 - name: Create test branch with a commit + if: ${{ inputs.mode == 'ref' }} run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" diff --git a/scripts/check_action_input_coverage.py b/scripts/check_action_input_coverage.py index 9167c6b..b27425b 100644 --- a/scripts/check_action_input_coverage.py +++ b/scripts/check_action_input_coverage.py @@ -36,13 +36,25 @@ def parse_action_inputs(action_file: Path) -> list[str]: if indent <= inputs_indent: break - match = re.match(r"^(\s+)([A-Za-z0-9_]+):\s*$", line) + match = re.match( + r"^(?P\s+)" + r"(?:\"(?P[^\"]+)\"|" + r"'(?P[^']+)'|" + r"(?P[A-Za-z0-9_-]+))" + r":\s*$", + line, + ) if not match: continue - key_indent = len(match.group(1)) + key_indent = len(match.group("indent")) if key_indent == inputs_indent + 2: - keys.append(match.group(2)) + key = ( + match.group("double_quoted") + or match.group("single_quoted") + or match.group("plain") + ) + keys.append(key) return keys @@ -57,11 +69,20 @@ def read_text_if_possible(path: Path) -> str: return "" +def normalize_input_env_name(input_name: str) -> str: + """Normalize an action input name to INPUT_* environment variable form.""" + normalized = re.sub(r"[^A-Za-z0-9]", "_", input_name).upper() + return f"INPUT_{normalized}" + + def input_is_covered(input_name: str, corpus: str) -> bool: """Check whether an input name appears in test corpus directly or as INPUT_* env.""" escaped = re.escape(input_name) - env_name = re.escape(f"INPUT_{input_name.upper()}") - pattern = re.compile(rf"\b{escaped}\b|\b{env_name}\b") + env_name = re.escape(normalize_input_env_name(input_name)) + pattern = re.compile( + rf"(? Date: Sun, 24 May 2026 18:33:06 +0200 Subject: [PATCH 11/15] feat: add validation for mode input and handle empty action repositories in workflows --- .github/workflows/e2e-action-commit-push.yml | 4 ++++ .github/workflows/e2e-action-terraform-copy-vars.yml | 10 ++++++++++ .github/workflows/e2e-action-terraform-validate.yml | 10 ++++++++++ .github/workflows/e2e-action-tflint.yml | 10 ++++++++++ scripts/check_action_input_coverage.py | 4 ++++ tests/test_check_action_input_coverage.py | 8 ++++++++ 6 files changed, 46 insertions(+) diff --git a/.github/workflows/e2e-action-commit-push.yml b/.github/workflows/e2e-action-commit-push.yml index c17fc0d..6c1182c 100644 --- a/.github/workflows/e2e-action-commit-push.yml +++ b/.github/workflows/e2e-action-commit-push.yml @@ -262,6 +262,7 @@ jobs: fetch-depth: 0 - name: Prepare base and target branches + if: ${{ inputs.mode == 'ref' }} run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" @@ -328,6 +329,7 @@ jobs: fetch-depth: 0 - name: Prepare conflicting branches + if: ${{ inputs.mode == 'ref' }} run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" @@ -394,6 +396,7 @@ jobs: fetch-depth: 0 - name: Create initial branch with commit + if: ${{ inputs.mode == 'ref' }} run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" @@ -404,6 +407,7 @@ jobs: git push origin test/e2e-commit-push-amend-${{ github.run_id }} - name: Add more content to file + if: ${{ inputs.mode == 'ref' }} run: echo "v2" >> e2e-amend-test.txt - name: Amend commit keeping original message diff --git a/.github/workflows/e2e-action-terraform-copy-vars.yml b/.github/workflows/e2e-action-terraform-copy-vars.yml index a7c27a8..67ab962 100644 --- a/.github/workflows/e2e-action-terraform-copy-vars.yml +++ b/.github/workflows/e2e-action-terraform-copy-vars.yml @@ -37,6 +37,16 @@ jobs: name: Copy variables from central file to modules runs-on: ubuntu-latest steps: + - name: Validate mode input + run: | + case "${{ inputs.mode }}" in + ref|image) ;; + *) + echo "Unsupported mode '${{ inputs.mode }}'. Expected 'ref' or 'image'." + exit 1 + ;; + esac + - name: Checkout repository uses: actions/checkout@v6 diff --git a/.github/workflows/e2e-action-terraform-validate.yml b/.github/workflows/e2e-action-terraform-validate.yml index 7bf0511..c1e59a4 100644 --- a/.github/workflows/e2e-action-terraform-validate.yml +++ b/.github/workflows/e2e-action-terraform-validate.yml @@ -37,6 +37,16 @@ jobs: name: Validate valid Terraform configuration runs-on: ubuntu-latest steps: + - name: Validate mode input + run: | + case "${{ inputs.mode }}" in + ref|image) ;; + *) + echo "Unsupported mode '${{ inputs.mode }}'. Expected 'ref' or 'image'." + exit 1 + ;; + esac + - name: Checkout repository uses: actions/checkout@v6 diff --git a/.github/workflows/e2e-action-tflint.yml b/.github/workflows/e2e-action-tflint.yml index b8a051e..465e3cc 100644 --- a/.github/workflows/e2e-action-tflint.yml +++ b/.github/workflows/e2e-action-tflint.yml @@ -37,6 +37,16 @@ jobs: name: Basic TFLint on valid Terraform files runs-on: ubuntu-latest steps: + - name: Validate mode input + run: | + case "${{ inputs.mode }}" in + ref|image) ;; + *) + echo "Unsupported mode '${{ inputs.mode }}'. Expected 'ref' or 'image'." + exit 1 + ;; + esac + - name: Checkout repository uses: actions/checkout@v6 diff --git a/scripts/check_action_input_coverage.py b/scripts/check_action_input_coverage.py index b27425b..49c1f49 100644 --- a/scripts/check_action_input_coverage.py +++ b/scripts/check_action_input_coverage.py @@ -195,6 +195,10 @@ def print_report(results: dict[str, list[str]]) -> None: def strict_gate(results: dict[str, list[str]], baseline_file: Path) -> int: """Apply strict gate against baseline and return process code.""" + if not results: + print("[ERROR] no action-* repositories discovered under --workspace-root") + return 1 + baseline = load_baseline(baseline_file) has_new_gap = False diff --git a/tests/test_check_action_input_coverage.py b/tests/test_check_action_input_coverage.py index 4704e96..3fc8d8d 100644 --- a/tests/test_check_action_input_coverage.py +++ b/tests/test_check_action_input_coverage.py @@ -63,6 +63,14 @@ def test_input_is_covered_by_env_name_for_hyphenated_input(self): corpus = "run: echo $INPUT_DRY_RUN_MODE" self.assertTrue(self.module.input_is_covered("dry-run-mode", corpus)) + def test_strict_gate_fails_when_no_action_repos(self): + """Strict gate fails when discovery yields no action repositories.""" + with tempfile.TemporaryDirectory() as tmp_dir: + baseline_file = Path(tmp_dir) / "baseline.json" + baseline_file.write_text("{}\n", encoding="utf-8") + exit_code = self.module.strict_gate({}, baseline_file) + self.assertEqual(exit_code, 1) + if __name__ == "__main__": unittest.main() From 212612de8db458675b9abccc5dd5bae26088fad3 Mon Sep 17 00:00:00 2001 From: ChristophShyper <45788587+ChristophShyper@users.noreply.github.com> Date: Sun, 24 May 2026 18:45:06 +0200 Subject: [PATCH 12/15] feat: add input validation for mode in end-to-end workflows and handle invalid JSON in baseline --- .github/workflows/e2e-action-commit-push.yml | 10 ++++++++++ .github/workflows/e2e-action-pull-request.yml | 10 ++++++++++ .github/workflows/e2e-action-template-action.yml | 10 ++++++++++ scripts/check_action_input_coverage.py | 6 +++++- tests/test_check_action_input_coverage.py | 8 ++++++++ 5 files changed, 43 insertions(+), 1 deletion(-) diff --git a/.github/workflows/e2e-action-commit-push.yml b/.github/workflows/e2e-action-commit-push.yml index 6c1182c..9517008 100644 --- a/.github/workflows/e2e-action-commit-push.yml +++ b/.github/workflows/e2e-action-commit-push.yml @@ -37,6 +37,16 @@ jobs: name: Basic commit and push to new branch runs-on: ubuntu-latest steps: + - name: Validate mode input + run: | + case "${{ inputs.mode }}" in + ref|image) ;; + *) + echo "Unsupported mode '${{ inputs.mode }}'. Expected 'ref' or 'image'." + exit 1 + ;; + esac + - name: Checkout repository uses: actions/checkout@v6 with: diff --git a/.github/workflows/e2e-action-pull-request.yml b/.github/workflows/e2e-action-pull-request.yml index 20a535e..8d43ac3 100644 --- a/.github/workflows/e2e-action-pull-request.yml +++ b/.github/workflows/e2e-action-pull-request.yml @@ -39,6 +39,16 @@ jobs: name: Basic pull request creation runs-on: ubuntu-latest steps: + - name: Validate mode input + run: | + case "${{ inputs.mode }}" in + ref|image) ;; + *) + echo "Unsupported mode '${{ inputs.mode }}'. Expected 'ref' or 'image'." + exit 1 + ;; + esac + - name: Checkout repository uses: actions/checkout@v6 with: diff --git a/.github/workflows/e2e-action-template-action.yml b/.github/workflows/e2e-action-template-action.yml index 13f4b6f..86c96a1 100644 --- a/.github/workflows/e2e-action-template-action.yml +++ b/.github/workflows/e2e-action-template-action.yml @@ -37,6 +37,16 @@ jobs: name: Validate template-action input/output contract runs-on: ubuntu-latest steps: + - name: Validate mode input + run: | + case "${{ inputs.mode }}" in + ref|image) ;; + *) + echo "Unsupported mode '${{ inputs.mode }}'. Expected 'ref' or 'image'." + exit 1 + ;; + esac + - name: Run template action in ref mode if: ${{ inputs.mode == 'ref' }} id: action diff --git a/scripts/check_action_input_coverage.py b/scripts/check_action_input_coverage.py index 49c1f49..571f0d0 100644 --- a/scripts/check_action_input_coverage.py +++ b/scripts/check_action_input_coverage.py @@ -106,7 +106,11 @@ def load_baseline(path: Path) -> dict[str, list[str]]: if not path.exists(): return {} - raw = json.loads(path.read_text(encoding="utf-8")) + try: + raw = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError: + print(f"[ERROR] invalid JSON in baseline file: {path}") + return {} if not isinstance(raw, dict): return {} diff --git a/tests/test_check_action_input_coverage.py b/tests/test_check_action_input_coverage.py index 3fc8d8d..1f4c32e 100644 --- a/tests/test_check_action_input_coverage.py +++ b/tests/test_check_action_input_coverage.py @@ -71,6 +71,14 @@ def test_strict_gate_fails_when_no_action_repos(self): exit_code = self.module.strict_gate({}, baseline_file) self.assertEqual(exit_code, 1) + def test_load_baseline_handles_invalid_json(self): + """Invalid baseline JSON returns empty baseline without crashing.""" + with tempfile.TemporaryDirectory() as tmp_dir: + baseline_file = Path(tmp_dir) / "baseline.json" + baseline_file.write_text("{invalid-json", encoding="utf-8") + baseline = self.module.load_baseline(baseline_file) + self.assertEqual(baseline, {}) + if __name__ == "__main__": unittest.main() From 9ac1c87c38ef1ea29b5a41aaf5b0a5a375fda9d8 Mon Sep 17 00:00:00 2001 From: ChristophShyper <45788587+ChristophShyper@users.noreply.github.com> Date: Sun, 24 May 2026 18:57:43 +0200 Subject: [PATCH 13/15] fix: validate mode input for end-to-end testing workflow --- .../workflows/e2e-action-container-structure-test.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/e2e-action-container-structure-test.yml b/.github/workflows/e2e-action-container-structure-test.yml index d6b50dd..1f1fe37 100644 --- a/.github/workflows/e2e-action-container-structure-test.yml +++ b/.github/workflows/e2e-action-container-structure-test.yml @@ -37,6 +37,16 @@ jobs: name: Basic test with text output format runs-on: ubuntu-latest steps: + - name: Validate mode input + run: | + case "${{ inputs.mode }}" in + ref|image) ;; + *) + echo "Unsupported mode '${{ inputs.mode }}'. Expected 'ref' or 'image'." + exit 1 + ;; + esac + - name: Checkout repository uses: actions/checkout@v6 From 527c29e5b1fbf921b4460274718ffdfbad1a1783 Mon Sep 17 00:00:00 2001 From: ChristophShyper <45788587+ChristophShyper@users.noreply.github.com> Date: Sun, 24 May 2026 19:07:34 +0200 Subject: [PATCH 14/15] fix: update Alpine image version to 3.23.4 in end-to-end testing workflows --- .../e2e-action-container-structure-test.yml | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/e2e-action-container-structure-test.yml b/.github/workflows/e2e-action-container-structure-test.yml index 1f1fe37..b3335d0 100644 --- a/.github/workflows/e2e-action-container-structure-test.yml +++ b/.github/workflows/e2e-action-container-structure-test.yml @@ -51,14 +51,14 @@ jobs: uses: actions/checkout@v6 - name: Pull Alpine test image - run: docker pull alpine:latest + run: docker pull alpine:3.23.4 - name: Run container structure tests with text output if: ${{ inputs.mode == 'ref' }} id: cst uses: devops-infra/action-container-structure-test@v1 with: - image: alpine:latest + image: alpine:3.23.4 config: tests/fixtures/container-structure-test/alpine.yml output: text @@ -90,14 +90,14 @@ jobs: uses: actions/checkout@v6 - name: Pull Alpine test image - run: docker pull alpine:latest + run: docker pull alpine:3.23.4 - name: Run container structure tests with JSON output if: ${{ inputs.mode == 'ref' }} id: cst uses: devops-infra/action-container-structure-test@v1 with: - image: alpine:latest + image: alpine:3.23.4 config: tests/fixtures/container-structure-test/alpine.yml output: json @@ -127,14 +127,14 @@ jobs: uses: actions/checkout@v6 - name: Pull Alpine test image - run: docker pull alpine:latest + run: docker pull alpine:3.23.4 - name: Run container structure tests with JUnit output if: ${{ inputs.mode == 'ref' }} id: cst uses: devops-infra/action-container-structure-test@v1 with: - image: alpine:latest + image: alpine:3.23.4 config: tests/fixtures/container-structure-test/alpine.yml output: junit junit_suite_name: e2e-alpine-tests @@ -165,14 +165,14 @@ jobs: uses: actions/checkout@v6 - name: Pull Alpine test image - run: docker pull alpine:latest + run: docker pull alpine:3.23.4 - name: Run container structure tests saving report to file if: ${{ inputs.mode == 'ref' }} id: cst uses: devops-infra/action-container-structure-test@v1 with: - image: alpine:latest + image: alpine:3.23.4 config: tests/fixtures/container-structure-test/alpine.yml output: json test_report: /tmp/cst-report.json @@ -207,14 +207,14 @@ jobs: uses: actions/checkout@v6 - name: Pull Alpine test image - run: docker pull alpine:latest + run: docker pull alpine:3.23.4 - name: Run container structure tests with multiple config files if: ${{ inputs.mode == 'ref' }} id: cst uses: devops-infra/action-container-structure-test@v1 with: - image: alpine:latest + image: alpine:3.23.4 config: | tests/fixtures/container-structure-test/alpine.yml tests/fixtures/container-structure-test/alpine-extended.yml @@ -248,7 +248,7 @@ jobs: uses: actions/checkout@v6 - name: Pull Alpine test image - run: docker pull alpine:latest + run: docker pull alpine:3.23.4 - name: Create metadata file run: | @@ -265,7 +265,7 @@ jobs: id: cst uses: devops-infra/action-container-structure-test@v1 with: - image: alpine:latest + image: alpine:3.23.4 config: tests/fixtures/container-structure-test/alpine.yml driver: docker platform: linux/amd64 From a9cede86e6d05a106cd6c3cae13d055379ffa22b Mon Sep 17 00:00:00 2001 From: ChristophShyper <45788587+ChristophShyper@users.noreply.github.com> Date: Sun, 24 May 2026 19:27:31 +0200 Subject: [PATCH 15/15] feat: add preflight validation step for workflow inputs in end-to-end testing --- .github/workflows/e2e-action-commit-push.yml | 16 ++++++++++++++-- .../e2e-action-container-structure-test.yml | 14 ++++++++++++-- .github/workflows/e2e-action-format-hcl.yml | 12 ++++++++++-- .github/workflows/e2e-action-pull-request.yml | 12 ++++++++++-- .github/workflows/e2e-action-template-action.yml | 10 ++++++++-- .../workflows/e2e-action-terraform-copy-vars.yml | 11 +++++++++-- .../workflows/e2e-action-terraform-validate.yml | 10 ++++++++-- .github/workflows/e2e-action-tflint.yml | 13 +++++++++++-- 8 files changed, 82 insertions(+), 16 deletions(-) diff --git a/.github/workflows/e2e-action-commit-push.yml b/.github/workflows/e2e-action-commit-push.yml index 9517008..9291b6a 100644 --- a/.github/workflows/e2e-action-commit-push.yml +++ b/.github/workflows/e2e-action-commit-push.yml @@ -33,8 +33,8 @@ permissions: contents: write jobs: - basic-commit: - name: Basic commit and push to new branch + preflight: + name: Validate workflow inputs runs-on: ubuntu-latest steps: - name: Validate mode input @@ -47,6 +47,11 @@ jobs: ;; esac + basic-commit: + name: Basic commit and push to new branch + needs: [preflight] + runs-on: ubuntu-latest + steps: - name: Checkout repository uses: actions/checkout@v6 with: @@ -88,6 +93,7 @@ jobs: commit-with-prefix-message: name: Commit with custom prefix and message + needs: [preflight] runs-on: ubuntu-latest steps: - name: Checkout repository @@ -132,6 +138,7 @@ jobs: allow-empty-commit: name: Allow empty commit with no file changes + needs: [preflight] runs-on: ubuntu-latest steps: - name: Checkout repository @@ -171,6 +178,7 @@ jobs: commit-with-timestamp-branch: name: Commit to timestamped branch name + needs: [preflight] runs-on: ubuntu-latest steps: - name: Checkout repository @@ -218,6 +226,7 @@ jobs: commit-with-repository-path: name: Commit from custom repository_path + needs: [preflight] runs-on: ubuntu-latest steps: - name: Checkout repository into custom path @@ -264,6 +273,7 @@ jobs: reset-target-branch-to-base: name: Reset target branch to base branch + needs: [preflight] runs-on: ubuntu-latest steps: - name: Checkout repository @@ -331,6 +341,7 @@ jobs: rebase-conflict-fails-when-strict: name: Rebase conflict fails when fail_on_rebase_conflict=true + needs: [preflight] runs-on: ubuntu-latest steps: - name: Checkout repository @@ -398,6 +409,7 @@ jobs: amend-commit: name: Amend previous commit with force-with-lease + needs: [preflight] runs-on: ubuntu-latest steps: - name: Checkout repository diff --git a/.github/workflows/e2e-action-container-structure-test.yml b/.github/workflows/e2e-action-container-structure-test.yml index b3335d0..695ab5a 100644 --- a/.github/workflows/e2e-action-container-structure-test.yml +++ b/.github/workflows/e2e-action-container-structure-test.yml @@ -33,8 +33,8 @@ permissions: contents: read jobs: - text-output: - name: Basic test with text output format + preflight: + name: Validate workflow inputs runs-on: ubuntu-latest steps: - name: Validate mode input @@ -47,6 +47,11 @@ jobs: ;; esac + text-output: + name: Basic test with text output format + needs: [preflight] + runs-on: ubuntu-latest + steps: - name: Checkout repository uses: actions/checkout@v6 @@ -84,6 +89,7 @@ jobs: json-output: name: Test with JSON output format + needs: [preflight] runs-on: ubuntu-latest steps: - name: Checkout repository @@ -121,6 +127,7 @@ jobs: junit-output: name: Test with JUnit output format and suite name + needs: [preflight] runs-on: ubuntu-latest steps: - name: Checkout repository @@ -159,6 +166,7 @@ jobs: test-report-file: name: Test with report written to file + needs: [preflight] runs-on: ubuntu-latest steps: - name: Checkout repository @@ -201,6 +209,7 @@ jobs: multiple-config-files: name: Test with multiple config files + needs: [preflight] runs-on: ubuntu-latest steps: - name: Checkout repository @@ -242,6 +251,7 @@ jobs: metadata-platform-runtime: name: Test with metadata, platform, and runtime inputs + needs: [preflight] runs-on: ubuntu-latest steps: - name: Checkout repository diff --git a/.github/workflows/e2e-action-format-hcl.yml b/.github/workflows/e2e-action-format-hcl.yml index 593fcbb..a8b8f8e 100644 --- a/.github/workflows/e2e-action-format-hcl.yml +++ b/.github/workflows/e2e-action-format-hcl.yml @@ -33,8 +33,8 @@ permissions: contents: read jobs: - format-check-clean-files: - name: Check mode on already-formatted files passes + preflight: + name: Validate workflow inputs runs-on: ubuntu-latest steps: - name: Validate mode input @@ -47,6 +47,11 @@ jobs: ;; esac + format-check-clean-files: + name: Check mode on already-formatted files passes + needs: [preflight] + runs-on: ubuntu-latest + steps: - name: Checkout repository uses: actions/checkout@v6 @@ -100,6 +105,7 @@ jobs: format-write-mode: name: Write mode formats and rewrites files + needs: [preflight] runs-on: ubuntu-latest steps: - name: Checkout repository @@ -160,6 +166,7 @@ jobs: format-check-malformed-files: name: Check mode on malformed files reports failure + needs: [preflight] runs-on: ubuntu-latest steps: - name: Checkout repository @@ -206,6 +213,7 @@ jobs: format-list-with-diff: name: List and diff mode on formatted files + needs: [preflight] runs-on: ubuntu-latest steps: - name: Checkout repository diff --git a/.github/workflows/e2e-action-pull-request.yml b/.github/workflows/e2e-action-pull-request.yml index 8d43ac3..f76cb34 100644 --- a/.github/workflows/e2e-action-pull-request.yml +++ b/.github/workflows/e2e-action-pull-request.yml @@ -35,8 +35,8 @@ permissions: issues: write jobs: - basic-pull-request: - name: Basic pull request creation + preflight: + name: Validate workflow inputs runs-on: ubuntu-latest steps: - name: Validate mode input @@ -49,6 +49,11 @@ jobs: ;; esac + basic-pull-request: + name: Basic pull request creation + needs: [preflight] + runs-on: ubuntu-latest + steps: - name: Checkout repository uses: actions/checkout@v6 with: @@ -106,6 +111,7 @@ jobs: pull-request-with-title-body: name: Pull request with custom title and body + needs: [preflight] runs-on: ubuntu-latest steps: - name: Checkout repository @@ -169,6 +175,7 @@ jobs: pull-request-with-repository-path: name: Pull request using custom checkout path and explicit repository + needs: [preflight] runs-on: ubuntu-latest steps: - name: Checkout repository into custom path @@ -233,6 +240,7 @@ jobs: pull-request-draft-with-diff: name: Draft pull request with get_diff enabled + needs: [preflight] runs-on: ubuntu-latest steps: - name: Checkout repository diff --git a/.github/workflows/e2e-action-template-action.yml b/.github/workflows/e2e-action-template-action.yml index 86c96a1..cfd14ae 100644 --- a/.github/workflows/e2e-action-template-action.yml +++ b/.github/workflows/e2e-action-template-action.yml @@ -33,8 +33,8 @@ permissions: contents: read jobs: - template-action-input-output: - name: Validate template-action input/output contract + preflight: + name: Validate workflow inputs runs-on: ubuntu-latest steps: - name: Validate mode input @@ -47,6 +47,11 @@ jobs: ;; esac + template-action-input-output: + name: Validate template-action input/output contract + needs: [preflight] + runs-on: ubuntu-latest + steps: - name: Run template action in ref mode if: ${{ inputs.mode == 'ref' }} id: action @@ -73,6 +78,7 @@ jobs: template-action-debug: name: Validate template-action debug mode + needs: [preflight] runs-on: ubuntu-latest steps: - name: Run template action with debug enabled diff --git a/.github/workflows/e2e-action-terraform-copy-vars.yml b/.github/workflows/e2e-action-terraform-copy-vars.yml index 67ab962..bad2d01 100644 --- a/.github/workflows/e2e-action-terraform-copy-vars.yml +++ b/.github/workflows/e2e-action-terraform-copy-vars.yml @@ -33,8 +33,8 @@ permissions: contents: read jobs: - basic-copy-vars: - name: Copy variables from central file to modules + preflight: + name: Validate workflow inputs runs-on: ubuntu-latest steps: - name: Validate mode input @@ -47,6 +47,11 @@ jobs: ;; esac + basic-copy-vars: + name: Copy variables from central file to modules + needs: [preflight] + runs-on: ubuntu-latest + steps: - name: Checkout repository uses: actions/checkout@v6 @@ -127,6 +132,7 @@ jobs: copy-vars-custom-paths: name: Copy variables with custom directory and file paths + needs: [preflight] runs-on: ubuntu-latest steps: - name: Checkout repository @@ -202,6 +208,7 @@ jobs: copy-vars-fail-on-missing: name: Fail when module uses variable missing from central file + needs: [preflight] runs-on: ubuntu-latest steps: - name: Checkout repository diff --git a/.github/workflows/e2e-action-terraform-validate.yml b/.github/workflows/e2e-action-terraform-validate.yml index c1e59a4..f9d9b48 100644 --- a/.github/workflows/e2e-action-terraform-validate.yml +++ b/.github/workflows/e2e-action-terraform-validate.yml @@ -33,8 +33,8 @@ permissions: contents: read jobs: - validate-basic: - name: Validate valid Terraform configuration + preflight: + name: Validate workflow inputs runs-on: ubuntu-latest steps: - name: Validate mode input @@ -47,6 +47,11 @@ jobs: ;; esac + validate-basic: + name: Validate valid Terraform configuration + needs: [preflight] + runs-on: ubuntu-latest + steps: - name: Checkout repository uses: actions/checkout@v6 @@ -98,6 +103,7 @@ jobs: validate-with-dir-filter: name: Validate with explicit directory filter + needs: [preflight] runs-on: ubuntu-latest steps: - name: Checkout repository diff --git a/.github/workflows/e2e-action-tflint.yml b/.github/workflows/e2e-action-tflint.yml index 465e3cc..c152808 100644 --- a/.github/workflows/e2e-action-tflint.yml +++ b/.github/workflows/e2e-action-tflint.yml @@ -33,8 +33,8 @@ permissions: contents: read jobs: - basic-tflint: - name: Basic TFLint on valid Terraform files + preflight: + name: Validate workflow inputs runs-on: ubuntu-latest steps: - name: Validate mode input @@ -47,6 +47,11 @@ jobs: ;; esac + basic-tflint: + name: Basic TFLint on valid Terraform files + needs: [preflight] + runs-on: ubuntu-latest + steps: - name: Checkout repository uses: actions/checkout@v6 @@ -96,6 +101,7 @@ jobs: tflint-with-dir-filter: name: TFLint with specific directory filter + needs: [preflight] runs-on: ubuntu-latest steps: - name: Checkout repository @@ -156,6 +162,7 @@ jobs: tflint-no-fail-on-changes: name: TFLint with fail_on_changes disabled + needs: [preflight] runs-on: ubuntu-latest steps: - name: Checkout repository @@ -205,6 +212,7 @@ jobs: tflint-with-custom-config: name: TFLint with explicit tflint_config + needs: [preflight] runs-on: ubuntu-latest steps: - name: Checkout repository @@ -264,6 +272,7 @@ jobs: tflint-with-custom-params: name: TFLint with custom tflint_params + needs: [preflight] runs-on: ubuntu-latest steps: - name: Checkout repository