From 94286b5e972d8efb6b3ace359a110ad0aa2bd833 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Bru=C5=BEina?= Date: Fri, 2 Oct 2026 08:50:36 +0200 Subject: [PATCH 1/3] feat: add terraform composite action --- .github/workflows/test.yaml | 30 +++++++++------ README.md | 75 +++++++++++++++++++++++++++++-------- action.yaml | 74 ++++++++++++++++++++++++++++++++++++ terraform/config.tf | 2 +- 4 files changed, 152 insertions(+), 29 deletions(-) create mode 100644 action.yaml diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index b628db6..8e99c9e 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -11,17 +11,23 @@ concurrency: jobs: terraform: name: Terraform - uses: ./.github/workflows/terraform.yaml + runs-on: ubuntu-latest permissions: contents: read - with: - aws_bucket: ${{ vars.AWS_TF_BUCKET }} - aws_endpoint_url_s3: ${{ vars.AWS_ENDPOINT_URL_S3 }} - gh_tf_owner: ${{ vars.GH_TF_OWNER }} - gh_tf_app_id: ${{ vars.GH_TF_APP_ID }} - gh_tf_app_installation_id: ${{ vars.GH_TF_APP_INSTALLATION_ID }} - path: test.yaml - secrets: - aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - gh_tf_app_pem_file: ${{ secrets.GH_TF_APP_PEM_FILE }} + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + persist-credentials: false + - name: Terraform + uses: ./ + with: + aws-bucket: ${{ vars.AWS_TF_BUCKET }} + aws-endpoint-url-s3: ${{ vars.AWS_ENDPOINT_URL_S3 }} + owner: ${{ vars.GH_TF_OWNER }} + app-id: ${{ vars.GH_TF_APP_ID }} + app-installation-id: ${{ vars.GH_TF_APP_INSTALLATION_ID }} + path: test.yaml + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + app-private-key: ${{ secrets.GH_TF_APP_PEM_FILE }} diff --git a/README.md b/README.md index 08f5182..8e88b06 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ GitOps workflow turning a declarative YAML organization definition into GitHub r - **Automated GitHub Organization management** - Define repositories using simple YAML file. - **Repository metadata** - Define description, homepage URL, topics. -- **Reusable GitOps Workflow** - Manage configurations using pull requests and automate updates using GitHub Actions. +- **GitOps Composite Action** - Manage configurations using pull requests and automate updates using a [composite action](action.yaml). - **Terraform** - Uses Terraform under the hood to apply changes efficiently. - **Terraform State Management** - Stores Terraform state securely in AWS S3. - **GitHub App Integration** - Uses a GitHub App for authentication and API interactions. @@ -55,9 +55,9 @@ repositories: - name: .github ``` -### GitHub Workflow +### Use Terraform Action -Create the workflow: +Create a workflow, for example, `.github/workflows/github-organization-as-code.yaml`: ```yaml --- @@ -68,22 +68,36 @@ on: branches: - main +concurrency: + group: ${{ github.workflow }} + jobs: - call-terraform: - uses: bruzit/github-organization-as-code/.github/workflows/terraform.yaml@v0 - with: - aws_bucket: ${{ vars.AWS_TF_BUCKET }} - aws_endpoint_url_s3: ${{ vars.AWS_ENDPOINT_URL_S3 }} - gh_tf_owner: ${{ vars.GH_TF_OWNER }} - gh_tf_app_id: ${{ vars.GH_TF_APP_ID }} - gh_tf_app_installation_id: ${{ vars.GH_TF_APP_INSTALLATION_ID }} - path: config.yaml - secrets: - aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - gh_tf_app_pem_file: ${{ secrets.GH_TF_APP_PEM_FILE }} + terraform: + name: Terraform + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + persist-credentials: false + - name: Terraform + uses: bruzit/github-organization-as-code@v0 + with: + aws-bucket: ${{ vars.AWS_TF_BUCKET }} + aws-endpoint-url-s3: ${{ vars.AWS_ENDPOINT_URL_S3 }} + owner: ${{ vars.GH_TF_OWNER }} + app-id: ${{ vars.GH_TF_APP_ID }} + app-installation-id: ${{ vars.GH_TF_APP_INSTALLATION_ID }} + path: config.yaml + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + app-private-key: ${{ secrets.GH_TF_APP_PEM_FILE }} ``` +The [action](action.yaml) runs the Terraform code shipped with the action against the configuration file at `path`, relative to the workspace, so the caller checks out its repository first. It sets up the latest Terraform, checks formatting, initializes the S3 backend in `aws-bucket`, selects the workspace named after `owner`, validates, and applies with `-auto-approve`. `concurrency` queues pushes instead of failing the apply on the state lock. + Set up GitHub actions, variables and secrets: - GitHub / _Repository_ / Settings @@ -101,6 +115,35 @@ Set up GitHub actions, variables and secrets: - `AWS_ENDPOINT_URL_S3` - `AWS_TF_BUCKET` (S3 bucket name for Terraform state) +### Use Terraform Workflow + +Similar to [Use Terraform Action](#use-terraform-action), with the reusable workflow: + +```yaml +--- +name: GitHub Organization as Code + +on: + push: + branches: + - main + +jobs: + call-terraform: + uses: bruzit/github-organization-as-code/.github/workflows/terraform.yaml@v0 + with: + aws_bucket: ${{ vars.AWS_TF_BUCKET }} + aws_endpoint_url_s3: ${{ vars.AWS_ENDPOINT_URL_S3 }} + gh_tf_owner: ${{ vars.GH_TF_OWNER }} + gh_tf_app_id: ${{ vars.GH_TF_APP_ID }} + gh_tf_app_installation_id: ${{ vars.GH_TF_APP_INSTALLATION_ID }} + path: config.yaml + secrets: + aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + gh_tf_app_pem_file: ${{ secrets.GH_TF_APP_PEM_FILE }} +``` + ## Usage ### GitHub Organization Configuration YAML diff --git a/action.yaml b/action.yaml new file mode 100644 index 0000000..40847fa --- /dev/null +++ b/action.yaml @@ -0,0 +1,74 @@ +--- +name: GitHub Organization as Code +description: Apply a GitHub organization YAML configuration with Terraform, authenticated by a GitHub App. +inputs: + aws-bucket: + required: true + description: S3 bucket name for Terraform state. + aws-endpoint-url-s3: + required: true + description: S3 endpoint URL. + owner: + required: true + description: GitHub organization to manage, also the Terraform workspace. + app-id: + required: true + description: GitHub App ID. + app-installation-id: + required: true + description: GitHub App installation ID. + path: + required: true + description: Organization configuration YAML, relative to the workspace. + aws-access-key-id: + required: true + description: S3 access key ID. + aws-secret-access-key: + required: true + description: S3 secret access key. + app-private-key: + required: true + description: GitHub App private key. +runs: + using: composite + steps: + - name: Set up Terraform + uses: hashicorp/setup-terraform@v4 + - name: Terraform fmt + shell: bash + run: terraform -chdir="$GITHUB_ACTION_PATH/terraform" fmt -check + - name: Terraform init + shell: bash + env: + AWS_BUCKET: ${{ inputs.aws-bucket }} + AWS_ENDPOINT_URL_S3: ${{ inputs.aws-endpoint-url-s3 }} + AWS_ACCESS_KEY_ID: ${{ inputs.aws-access-key-id }} + AWS_SECRET_ACCESS_KEY: ${{ inputs.aws-secret-access-key }} + TF_WORKSPACE: ${{ inputs.owner }} + TF_IN_AUTOMATION: true + run: terraform -chdir="$GITHUB_ACTION_PATH/terraform" init -input=false -backend-config="bucket=$AWS_BUCKET" + - name: Terraform validate + shell: bash + env: + GITHUB_APP_ID: ${{ inputs.app-id }} + GITHUB_APP_INSTALLATION_ID: ${{ inputs.app-installation-id }} + GITHUB_APP_PEM_FILE: | + ${{ inputs.app-private-key }} + TF_WORKSPACE: ${{ inputs.owner }} + TF_IN_AUTOMATION: true + run: terraform -chdir="$GITHUB_ACTION_PATH/terraform" validate + - name: Terraform apply + shell: bash + env: + AWS_ENDPOINT_URL_S3: ${{ inputs.aws-endpoint-url-s3 }} + AWS_ACCESS_KEY_ID: ${{ inputs.aws-access-key-id }} + AWS_SECRET_ACCESS_KEY: ${{ inputs.aws-secret-access-key }} + GITHUB_OWNER: ${{ inputs.owner }} + GITHUB_APP_ID: ${{ inputs.app-id }} + GITHUB_APP_INSTALLATION_ID: ${{ inputs.app-installation-id }} + GITHUB_APP_PEM_FILE: | + ${{ inputs.app-private-key }} + CONFIG_PATH: ${{ inputs.path }} + TF_WORKSPACE: ${{ inputs.owner }} + TF_IN_AUTOMATION: true + run: TF_VAR_config="$GITHUB_WORKSPACE/$CONFIG_PATH" terraform -chdir="$GITHUB_ACTION_PATH/terraform" apply -auto-approve -input=false diff --git a/terraform/config.tf b/terraform/config.tf index ac88258..91901d4 100644 --- a/terraform/config.tf +++ b/terraform/config.tf @@ -9,7 +9,7 @@ terraform { } backend "s3" { - # bucket is supplied at init time via -backend-config (see .github/workflows/terraform.yaml) + # bucket is supplied at init time via -backend-config (see action.yaml) workspace_key_prefix = "" key = "terraform.tfstate" use_lockfile = true # Set to false only for non-AWS S3 compatible APIs without "conditional object PUTs" capability From 1b840e4aaaf22ddc29f68a89de3899690399d6af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Bru=C5=BEina?= Date: Fri, 2 Oct 2026 08:53:13 +0200 Subject: [PATCH 2/3] docs: drop permissions from terraform action example --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 8e88b06..79831d6 100644 --- a/README.md +++ b/README.md @@ -75,8 +75,6 @@ jobs: terraform: name: Terraform runs-on: ubuntu-latest - permissions: - contents: read steps: - name: Checkout uses: actions/checkout@v7 From 8b42e8641c65d11459fcf4b85844e42b0c42a37a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Bru=C5=BEina?= Date: Fri, 2 Oct 2026 09:04:38 +0200 Subject: [PATCH 3/3] refactor: order terraform action inputs, rename app-private-key to app-pem-file --- .github/workflows/test.yaml | 8 ++++---- README.md | 16 ++++++++-------- action.yaml | 34 +++++++++++++++++----------------- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 8e99c9e..7d3c40a 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -22,12 +22,12 @@ jobs: - name: Terraform uses: ./ with: - aws-bucket: ${{ vars.AWS_TF_BUCKET }} - aws-endpoint-url-s3: ${{ vars.AWS_ENDPOINT_URL_S3 }} + path: test.yaml owner: ${{ vars.GH_TF_OWNER }} app-id: ${{ vars.GH_TF_APP_ID }} app-installation-id: ${{ vars.GH_TF_APP_INSTALLATION_ID }} - path: test.yaml + app-pem-file: ${{ secrets.GH_TF_APP_PEM_FILE }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - app-private-key: ${{ secrets.GH_TF_APP_PEM_FILE }} + aws-bucket: ${{ vars.AWS_TF_BUCKET }} + aws-endpoint-url-s3: ${{ vars.AWS_ENDPOINT_URL_S3 }} diff --git a/README.md b/README.md index 79831d6..d266bf0 100644 --- a/README.md +++ b/README.md @@ -83,15 +83,15 @@ jobs: - name: Terraform uses: bruzit/github-organization-as-code@v0 with: - aws-bucket: ${{ vars.AWS_TF_BUCKET }} - aws-endpoint-url-s3: ${{ vars.AWS_ENDPOINT_URL_S3 }} + path: config.yaml owner: ${{ vars.GH_TF_OWNER }} app-id: ${{ vars.GH_TF_APP_ID }} app-installation-id: ${{ vars.GH_TF_APP_INSTALLATION_ID }} - path: config.yaml + app-pem-file: ${{ secrets.GH_TF_APP_PEM_FILE }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - app-private-key: ${{ secrets.GH_TF_APP_PEM_FILE }} + aws-bucket: ${{ vars.AWS_TF_BUCKET }} + aws-endpoint-url-s3: ${{ vars.AWS_ENDPOINT_URL_S3 }} ``` The [action](action.yaml) runs the Terraform code shipped with the action against the configuration file at `path`, relative to the workspace, so the caller checks out its repository first. It sets up the latest Terraform, checks formatting, initializes the S3 backend in `aws-bucket`, selects the workspace named after `owner`, validates, and applies with `-auto-approve`. `concurrency` queues pushes instead of failing the apply on the state lock. @@ -130,16 +130,16 @@ jobs: call-terraform: uses: bruzit/github-organization-as-code/.github/workflows/terraform.yaml@v0 with: - aws_bucket: ${{ vars.AWS_TF_BUCKET }} - aws_endpoint_url_s3: ${{ vars.AWS_ENDPOINT_URL_S3 }} + path: config.yaml gh_tf_owner: ${{ vars.GH_TF_OWNER }} gh_tf_app_id: ${{ vars.GH_TF_APP_ID }} gh_tf_app_installation_id: ${{ vars.GH_TF_APP_INSTALLATION_ID }} - path: config.yaml + aws_bucket: ${{ vars.AWS_TF_BUCKET }} + aws_endpoint_url_s3: ${{ vars.AWS_ENDPOINT_URL_S3 }} secrets: + gh_tf_app_pem_file: ${{ secrets.GH_TF_APP_PEM_FILE }} aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - gh_tf_app_pem_file: ${{ secrets.GH_TF_APP_PEM_FILE }} ``` ## Usage diff --git a/action.yaml b/action.yaml index 40847fa..5ff1f01 100644 --- a/action.yaml +++ b/action.yaml @@ -2,12 +2,9 @@ name: GitHub Organization as Code description: Apply a GitHub organization YAML configuration with Terraform, authenticated by a GitHub App. inputs: - aws-bucket: - required: true - description: S3 bucket name for Terraform state. - aws-endpoint-url-s3: + path: required: true - description: S3 endpoint URL. + description: Organization configuration YAML, relative to the workspace. owner: required: true description: GitHub organization to manage, also the Terraform workspace. @@ -17,18 +14,21 @@ inputs: app-installation-id: required: true description: GitHub App installation ID. - path: + app-pem-file: required: true - description: Organization configuration YAML, relative to the workspace. + description: GitHub App private key (PEM file content). aws-access-key-id: required: true description: S3 access key ID. aws-secret-access-key: required: true description: S3 secret access key. - app-private-key: + aws-bucket: + required: true + description: S3 bucket name for Terraform state. + aws-endpoint-url-s3: required: true - description: GitHub App private key. + description: S3 endpoint URL. runs: using: composite steps: @@ -40,10 +40,10 @@ runs: - name: Terraform init shell: bash env: - AWS_BUCKET: ${{ inputs.aws-bucket }} - AWS_ENDPOINT_URL_S3: ${{ inputs.aws-endpoint-url-s3 }} AWS_ACCESS_KEY_ID: ${{ inputs.aws-access-key-id }} AWS_SECRET_ACCESS_KEY: ${{ inputs.aws-secret-access-key }} + AWS_BUCKET: ${{ inputs.aws-bucket }} + AWS_ENDPOINT_URL_S3: ${{ inputs.aws-endpoint-url-s3 }} TF_WORKSPACE: ${{ inputs.owner }} TF_IN_AUTOMATION: true run: terraform -chdir="$GITHUB_ACTION_PATH/terraform" init -input=false -backend-config="bucket=$AWS_BUCKET" @@ -53,22 +53,22 @@ runs: GITHUB_APP_ID: ${{ inputs.app-id }} GITHUB_APP_INSTALLATION_ID: ${{ inputs.app-installation-id }} GITHUB_APP_PEM_FILE: | - ${{ inputs.app-private-key }} + ${{ inputs.app-pem-file }} TF_WORKSPACE: ${{ inputs.owner }} TF_IN_AUTOMATION: true run: terraform -chdir="$GITHUB_ACTION_PATH/terraform" validate - name: Terraform apply shell: bash env: - AWS_ENDPOINT_URL_S3: ${{ inputs.aws-endpoint-url-s3 }} - AWS_ACCESS_KEY_ID: ${{ inputs.aws-access-key-id }} - AWS_SECRET_ACCESS_KEY: ${{ inputs.aws-secret-access-key }} + CONFIG_PATH: ${{ inputs.path }} GITHUB_OWNER: ${{ inputs.owner }} GITHUB_APP_ID: ${{ inputs.app-id }} GITHUB_APP_INSTALLATION_ID: ${{ inputs.app-installation-id }} GITHUB_APP_PEM_FILE: | - ${{ inputs.app-private-key }} - CONFIG_PATH: ${{ inputs.path }} + ${{ inputs.app-pem-file }} + AWS_ACCESS_KEY_ID: ${{ inputs.aws-access-key-id }} + AWS_SECRET_ACCESS_KEY: ${{ inputs.aws-secret-access-key }} + AWS_ENDPOINT_URL_S3: ${{ inputs.aws-endpoint-url-s3 }} TF_WORKSPACE: ${{ inputs.owner }} TF_IN_AUTOMATION: true run: TF_VAR_config="$GITHUB_WORKSPACE/$CONFIG_PATH" terraform -chdir="$GITHUB_ACTION_PATH/terraform" apply -auto-approve -input=false