Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 156 additions & 0 deletions .github/workflows/release-terraform-environment-v1.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
name: Terraform Environment Release v1
run-name: Terraform release ${{ inputs.project }}

on:
workflow_dispatch:
inputs:
project:
description: "Nx Terraform environment project to release"
required: true
type: string
project-root:
description: "Repository path containing the Terraform environment.json manifest"
required: true
type: string
source-ref:
description: "Git commit containing the released infrastructure configuration"
required: true
type: string

concurrency:
group: infra-terraform-${{ inputs.project }}
cancel-in-progress: false

jobs:
validate:
name: Validate Deployment Environment
runs-on: ubuntu-24.04
permissions:
contents: read
outputs:
apply-environment: ${{ fromJSON(steps.release-metadata.outputs.result).applyEnvironment }}
plan-environment: ${{ fromJSON(steps.release-metadata.outputs.result).planEnvironment }}
runner-label: ${{ fromJSON(steps.release-metadata.outputs.result).runnerLabel }}
source-ref: ${{ fromJSON(steps.release-metadata.outputs.result).sourceRef }}
steps:
- name: Validate release source and deployment metadata
id: release-metadata
uses: pagopa/dx/actions/run-dx-task@main
env:
GITHUB_TOKEN: ${{ github.token }}
with:
task: validateTerraformEnvironmentRelease
payload: >-
{
"owner": ${{ toJSON(github.repository_owner) }},
"project": ${{ toJSON(inputs.project) }},
"projectRoot": ${{ toJSON(inputs.project-root) }},
"repo": ${{ toJSON(github.event.repository.name) }},
"sourceRef": ${{ toJSON(inputs.source-ref) }}
}

plan:
name: Terraform Plan
needs: validate
runs-on: ["self-hosted", "${{ needs.validate.outputs.runner-label }}"]
environment: ${{ needs.validate.outputs.plan-environment }}
permissions:
actions: read
contents: read
id-token: write
env:
ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }}
ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }}
ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }}
ARM_USE_OIDC: true
ARM_USE_AZUREAD: true
ARM_STORAGE_USE_AZUREAD: true
ROLE_ARN: ${{ secrets.ROLE_ARN }}
AWS_REGION: ${{ secrets.AWS_REGION }}
steps:
- name: Check out released configuration
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
filter: tree:0
ref: ${{ needs.validate.outputs.source-ref }}

- name: Cloud Login
uses: pagopa/dx/actions/csp-login@main

- name: Setup Node.js
id: node-setup
uses: pagopa/dx/.github/actions/node-setup@main

- name: Setup Terraform
uses: pagopa/dx/.github/actions/terraform-setup@main

- name: Install dependencies (pnpm)
if: steps.node-setup.outputs.package-manager == 'pnpm'
run: pnpm install --frozen-lockfile

- name: Install dependencies (yarn)
if: steps.node-setup.outputs.package-manager == 'yarn'
run: yarn install --immutable

- name: Install dependencies (npm)
if: steps.node-setup.outputs.package-manager == 'npm'
run: npm ci

- name: Run Terraform plan-upload
env:
NX_PROJECT: ${{ inputs.project }}
run: npx nx run "$NX_PROJECT:tf-plan-upload" -c ci

apply:
name: Terraform Apply
needs: [plan, validate]
runs-on: ["self-hosted", "${{ needs.validate.outputs.runner-label }}"]
environment: ${{ needs.validate.outputs.apply-environment }}
permissions:
actions: read
contents: read
id-token: write
env:
ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }}
ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }}
ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }}
ARM_USE_OIDC: true
ARM_USE_AZUREAD: true
ARM_STORAGE_USE_AZUREAD: true
ROLE_ARN: ${{ secrets.ROLE_ARN }}
AWS_REGION: ${{ secrets.AWS_REGION }}
steps:
- name: Check out released configuration
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
filter: tree:0
ref: ${{ needs.validate.outputs.source-ref }}

- name: Cloud Login
uses: pagopa/dx/actions/csp-login@main

- name: Setup Node.js
id: node-setup
uses: pagopa/dx/.github/actions/node-setup@main

- name: Setup Terraform
uses: pagopa/dx/.github/actions/terraform-setup@main

- name: Install dependencies (pnpm)
if: steps.node-setup.outputs.package-manager == 'pnpm'
run: pnpm install --frozen-lockfile

- name: Install dependencies (yarn)
if: steps.node-setup.outputs.package-manager == 'yarn'
run: yarn install --immutable

- name: Install dependencies (npm)
if: steps.node-setup.outputs.package-manager == 'npm'
run: npm ci

- name: Apply reviewed Terraform plan
env:
NX_PROJECT: ${{ inputs.project }}
run: npx nx release publish --projects="$NX_PROJECT"
5 changes: 5 additions & 0 deletions .nx/version-plans/version-plan-1784227055723.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@pagopa/dx-tasks': minor
---

Validate trusted Terraform environment release metadata through dx-tasks.
6 changes: 6 additions & 0 deletions .nx/version-plans/version-plan-1784227063140.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
run-dx-task: minor
---

Expose JSON task results and an explicit GitHub token input from the run-dx-task
action.
8 changes: 8 additions & 0 deletions actions/run-dx-task/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,22 @@ Dispatches `@pagopa/dx-tasks` tasks from GitHub Actions by passing a task name a

## Inputs

- `github-token`: optional GitHub token passed to tasks that call GitHub. When
omitted, the action falls back to the `GITHUB_TOKEN` environment variable.
- `task`: dx-tasks task name to dispatch.
- `payload`: JSON payload consumed by the selected task.

## Outputs

- `result`: JSON-serialized value returned by the selected task. Tasks without
a return value produce `null`.

## Example

```yaml
- uses: pagopa/dx/actions/run-dx-task@main
with:
github-token: ${{ github.token }}
task: terraformPlan
payload: >-
{"modulePath":"infra/resources/example","refresh":false,"verbose":true}
Expand Down
7 changes: 7 additions & 0 deletions actions/run-dx-task/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,20 @@ name: Run DX Task
description: Dispatches DX task

inputs:
github-token:
description: GitHub token passed to tasks that call GitHub. Falls back to the GITHUB_TOKEN environment variable.
required: false
task:
description: Task name to run.
required: true
payload:
description: JSON payload passed to the selected task.
required: true
Comment thread
gunzip marked this conversation as resolved.

outputs:
result:
description: JSON-serialized result returned by the dispatched task.

runs:
using: node24
main: dist/index.mjs
1 change: 1 addition & 0 deletions actions/run-dx-task/dist/es5-BIm4_leK.mjs

Large diffs are not rendered by default.

218 changes: 193 additions & 25 deletions actions/run-dx-task/dist/index.mjs

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions actions/run-dx-task/dist/rolldown-runtime-DaDcL9Zw.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import{createRequire as e}from"node:module";var t=Object.create,n=Object.defineProperty,r=Object.getOwnPropertyDescriptor,i=Object.getOwnPropertyNames,a=Object.getPrototypeOf,o=Object.prototype.hasOwnProperty,s=(e,t,n)=>()=>{if(n)throw n[0];try{return e&&(t=e(e=0)),t}catch(e){throw n=[e],e}},c=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports),l=(e,t)=>{let r={};for(var i in e)n(r,i,{get:e[i],enumerable:!0});return t||n(r,Symbol.toStringTag,{value:`Module`}),r},u=(e,t,a,s)=>{if(t&&typeof t==`object`||typeof t==`function`)for(var c=i(t),l=0,u=c.length,d;l<u;l++)d=c[l],!o.call(e,d)&&d!==a&&n(e,d,{get:(e=>t[e]).bind(null,d),enumerable:!(s=r(t,d))||s.enumerable});return e},d=(e,r,i)=>(i=e==null?{}:t(a(e)),u(r||!e||!e.__esModule?n(i,`default`,{value:e,enumerable:!0}):i,e)),f=e=>o.call(e,`module.exports`)?e[`module.exports`]:u(n({},`__esModule`,{value:!0}),e),p=e(import.meta.url);export{f as a,p as i,s as n,d as o,l as r,c as t};
4 changes: 3 additions & 1 deletion actions/run-dx-task/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"tasks"
],
"scripts": {
"build": "tsdown"
"build": "tsdown",
"test": "vitest run"
},
"devDependencies": {
"@actions/core": "catalog:github-actions",
Expand All @@ -24,6 +25,7 @@
"prettier": "catalog:",
"tsdown": "catalog:",
"typescript": "catalog:",
"vitest": "catalog:",
"zod": "catalog:"
},
"dependencies": {
Expand Down
23 changes: 23 additions & 0 deletions actions/run-dx-task/src/__tests__/github-token.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/** Verifies GitHub token selection for the run-dx-task action. */

import { describe, expect, it } from "vitest";

import { resolveGitHubToken } from "../github-token.js";

describe("resolveGitHubToken", () => {
it("prefers the explicit action input", () => {
expect(resolveGitHubToken("input-token", "environment-token")).toBe(
"input-token",
);
});

it("falls back to GITHUB_TOKEN when the input is empty", () => {
expect(resolveGitHubToken("", "environment-token")).toBe(
"environment-token",
);
});

it("returns undefined when neither source provides a token", () => {
expect(resolveGitHubToken("", undefined)).toBeUndefined();
});
});
6 changes: 6 additions & 0 deletions actions/run-dx-task/src/github-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/** Resolves the GitHub token while preserving the action's legacy environment contract. */

export const resolveGitHubToken = (
inputToken: string,
environmentToken: string | undefined,
): string | undefined => inputToken || environmentToken;
19 changes: 17 additions & 2 deletions actions/run-dx-task/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@ import * as core from "@actions/core";
import { createDefaultTaskDispatcher } from "@pagopa/dx-tasks/default-dispatcher";
import * as z from "zod/mini";

import { resolveGitHubToken } from "./github-token.js";

const actionInputsSchema = z.object({
payload: z.string().check(z.minLength(1, "Payload cannot be empty")),
task: z.string().check(z.minLength(1, "Task name cannot be empty")),
});
const githubTokenSchema = z.optional(
z.string().check(z.minLength(1, "GITHUB_TOKEN cannot be empty")),
);

const parsePayload = (rawPayload: string): unknown => {
try {
Expand All @@ -30,10 +35,20 @@ async function run(): Promise<void> {
throw new Error(z.prettifyError(inputsResult.error));
}

const dispatcher = createDefaultTaskDispatcher();
const githubTokenResult = githubTokenSchema.safeParse(
resolveGitHubToken(core.getInput("github-token"), process.env.GITHUB_TOKEN),
);
if (!githubTokenResult.success) {
throw new Error(z.prettifyError(githubTokenResult.error));
}

const dispatcher = createDefaultTaskDispatcher({
githubToken: githubTokenResult.data,
});
const payload = parsePayload(inputsResult.data.payload);
const result = await dispatcher.dispatchTask(inputsResult.data.task, payload);

await dispatcher.dispatchTask(inputsResult.data.task, payload);
core.setOutput("result", JSON.stringify(result ?? null));
}

run().catch((error) => {
Expand Down
11 changes: 11 additions & 0 deletions actions/run-dx-task/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/** Configures focused unit tests for the run-dx-task action. */

import { defineConfig } from "vitest/config";

export default defineConfig({
test: {
environment: "node",
include: ["src/**/*.test.ts"],
watch: false,
},
});
17 changes: 9 additions & 8 deletions packages/dx-tasks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ Reusable task implementations and a small dispatcher for DX orchestration tools.

## Available tasks

| Task | Description |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `terraformPlan` | Runs `terraform plan` for a module path, handles common flags, and masks sensitive output before printing it. |
| `terraformPlanUpload` | Runs `terraformPlan` with a fixed output path, then uploads the resulting plan bundle (plan file, lock file, and module cache) to the same cloud storage backend used for the Terraform state, so it can be applied later by `terraformApply`. |
| `terraformApply` | Downloads the plan bundle uploaded by `terraformPlanUpload` (recomputing its deterministic storage path from the Terraform backend state and the current CI run), applies it non-interactively, and deletes the remote bundle only after a **successful** apply. |
| `renderReport` | Reads the persisted reports under `.dx-tasks` and renders them in a target format (currently `markdown`) to stdout, using per-namespace renderers. |
| `prComment` | Adds a comment to a GitHub pull request, optionally replacing existing comments that match a search pattern. |
| `reportPrComment` | Renders persisted reports and posts the rendered Markdown as a GitHub pull request comment. |
| Task | Description |
| ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `terraformPlan` | Runs `terraform plan` for a module path, handles common flags, and masks sensitive output before printing it. |
| `terraformPlanUpload` | Runs `terraformPlan` with a fixed output path, then uploads the resulting plan bundle (plan file, lock file, and module cache) to the same cloud storage backend used for the Terraform state, so it can be applied later by `terraformApply`. |
| `terraformApply` | Downloads the plan bundle uploaded by `terraformPlanUpload` (recomputing its deterministic storage path from the Terraform backend state and the current CI run), applies it non-interactively, and deletes the remote bundle only after a **successful** apply. |
| `validateTerraformEnvironmentRelease` | Validates that a Terraform environment release uses a commit reachable from the protected default branch and resolves its trusted deployment metadata. |
| `renderReport` | Reads the persisted reports under `.dx-tasks` and renders them in a target format (currently `markdown`) to stdout, using per-namespace renderers. |
| `prComment` | Adds a comment to a GitHub pull request, optionally replacing existing comments that match a search pattern. |
| `reportPrComment` | Renders persisted reports and posts the rendered Markdown as a GitHub pull request comment. |

## Dispatcher

Expand Down
Loading
Loading