Skip to content
This repository was archived by the owner on Apr 27, 2026. It is now read-only.

Commit a2abc88

Browse files
authored
feat: add GitHub Action for plan/apply/destroy/fmt/validate (#6)
* feat: add GitHub Action for plan/apply/destroy/fmt/validate Composite action at repo root enabling: - vmvarela/ghoten@v1 (with floating v1/v1.x tags via release workflow) - Auto-install binary from GitHub Releases - Automatic GHCR authentication - Zero-config ORAS backend (ghcr.io/<owner>/tf-state.<repo>) - Sensible defaults: gzip compression, lock_ttl=300, max_versions=10 - PR comments with plan output (deduplicated per dir/workspace/command) - Job Summary with resource change tables - Workspace-aware (TF_WORKSPACE from input) - Commands: plan, apply, destroy, fmt, validate Files: action.yml - action metadata and composite steps action/setup.sh - binary installation (multi-OS/arch) action/auth.sh - GHCR Docker config authentication action/init.sh - auto-init with ORAS backend defaults action/run.sh - command execution with detailed-exitcode action/comment.sh - PR comment creation/update via GitHub API action/summary.sh - Job Summary generation .github/workflows/release.yaml - floating version tags (v1, v1.x) README.md - GitHub Action documentation section * ci: add github-action label and release notes category * fix: replace broken proto symlinks with actual files * fix: address PR review comments from Copilot * style: remove redundant Ghoten prefix from PR comment and summary titles * docs: refactor README — concise, complete, and fix tf-state default
1 parent 589e590 commit a2abc88

13 files changed

Lines changed: 2801 additions & 210 deletions

File tree

.github/labeler.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,12 @@ area/diagnostics:
7777

7878
# ─── Tipo de cambio ──────────────────────────────────────────
7979

80+
github-action:
81+
- changed-files:
82+
- any-glob-to-any-file:
83+
- action.yml
84+
- action/**
85+
8086
docs:
8187
- changed-files:
8288
- any-glob-to-any-file:

.github/release.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ changelog:
1717
- title: "🐛 Bug Fixes"
1818
labels:
1919
- bug
20+
- title: "🎬 GitHub Action"
21+
labels:
22+
- github-action
2023
- title: "📖 Documentation"
2124
labels:
2225
- documentation

.github/workflows/release.yaml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,3 +287,44 @@ jobs:
287287
org.opencontainers.image.source=https://github.com/${{ github.repository }}
288288
org.opencontainers.image.licenses=MPL-2.0
289289
org.opencontainers.image.version=${{ needs.validate.outputs.version }}
290+
291+
# ---------------------------------------------------------------------------
292+
# Floating version tags — enables vmvarela/ghoten@v1 and @v1.12
293+
# ---------------------------------------------------------------------------
294+
update-action-tags:
295+
name: Update Action Version Tags
296+
runs-on: ubuntu-latest
297+
needs: [validate, aggregate]
298+
if: needs.validate.outputs.is_release == 'true' && needs.validate.outputs.is_prerelease != 'true'
299+
permissions:
300+
contents: write
301+
steps:
302+
- name: Checkout
303+
uses: actions/checkout@v6
304+
with:
305+
ref: ${{ needs.validate.outputs.tag }}
306+
fetch-depth: 0
307+
308+
- name: Update floating tags
309+
env:
310+
TAG: ${{ needs.validate.outputs.tag }}
311+
run: |
312+
# Note: this job only runs for stable releases
313+
# (see: needs.validate.outputs.is_prerelease != 'true' in the job condition),
314+
# so prerelease tags like v1.12.0-rc1 are excluded and these patterns are safe.
315+
# Parse semver: v1.12.0 → major=v1, minor=v1.12
316+
MAJOR=$(echo "$TAG" | grep -oE '^v[0-9]+')
317+
MINOR=$(echo "$TAG" | grep -oE '^v[0-9]+\.[0-9]+')
318+
319+
git config user.name "github-actions[bot]"
320+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
321+
322+
# Force-update major tag (v1)
323+
git tag -fa "$MAJOR" -m "Release ${TAG}" "$TAG"
324+
git push -f origin "$MAJOR"
325+
echo "✅ Updated ${MAJOR} → ${TAG}"
326+
327+
# Force-update minor tag (v1.12)
328+
git tag -fa "$MINOR" -m "Release ${TAG}" "$TAG"
329+
git push -f origin "$MINOR"
330+
echo "✅ Updated ${MINOR} → ${TAG}"

README.md

Lines changed: 211 additions & 208 deletions
Large diffs are not rendered by default.

action.yml

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
name: 'Ghoten'
2+
description: 'Run Ghoten (OpenTofu + ORAS) with automatic GHCR auth, PR comments, and Job Summaries'
3+
author: 'Victor M. Varela'
4+
5+
branding:
6+
icon: 'terminal'
7+
color: 'purple'
8+
9+
inputs:
10+
command:
11+
description: 'Command to run: plan, apply, destroy, fmt, validate'
12+
required: false
13+
default: 'plan'
14+
working-directory:
15+
description: 'Working directory containing HCL configuration files'
16+
required: false
17+
default: '.'
18+
workspace:
19+
description: 'Terraform workspace name'
20+
required: false
21+
default: 'default'
22+
var-file:
23+
description: 'Path to a .tfvars variable file'
24+
required: false
25+
default: ''
26+
variables:
27+
description: 'Newline-separated key=value variable pairs'
28+
required: false
29+
default: ''
30+
backend-repository:
31+
description: 'ORAS backend OCI repository (default: ghcr.io/<owner>/tf-state.<repo>)'
32+
required: false
33+
default: ''
34+
backend-config:
35+
description: 'Newline-separated backend-config key=value pairs'
36+
required: false
37+
default: ''
38+
github-token:
39+
description: 'GitHub token for GHCR authentication and PR comments (defaults to github.token when not set)'
40+
required: false
41+
default: ''
42+
comment-on-pr:
43+
description: 'Post/update command output as a PR comment'
44+
required: false
45+
default: 'true'
46+
add-summary:
47+
description: 'Generate a GitHub Job Summary'
48+
required: false
49+
default: 'true'
50+
args:
51+
description: 'Additional CLI arguments appended to the command'
52+
required: false
53+
default: ''
54+
init-args:
55+
description: 'Additional arguments for ghoten init'
56+
required: false
57+
default: ''
58+
auto-init:
59+
description: 'Automatically run ghoten init before the command'
60+
required: false
61+
default: 'true'
62+
compression:
63+
description: 'ORAS backend state compression: none or gzip'
64+
required: false
65+
default: 'gzip'
66+
lock-ttl:
67+
description: 'ORAS backend lock TTL in seconds (0 to disable)'
68+
required: false
69+
default: '300'
70+
max-versions:
71+
description: 'ORAS backend state versions to retain (0 to disable)'
72+
required: false
73+
default: '10'
74+
fmt-check:
75+
description: 'Use -check mode for the fmt command (report only, do not rewrite)'
76+
required: false
77+
default: 'false'
78+
version:
79+
description: 'Ghoten version to install (default: matches the action release)'
80+
required: false
81+
default: ''
82+
83+
outputs:
84+
exitcode:
85+
description: 'Exit code of the ghoten command'
86+
value: ${{ steps.run.outputs.exitcode }}
87+
stdout:
88+
description: 'Full command output'
89+
value: ${{ steps.run.outputs.stdout }}
90+
plan-has-changes:
91+
description: 'Whether plan detected infrastructure changes (true/false)'
92+
value: ${{ steps.run.outputs.plan_has_changes }}
93+
plan-file:
94+
description: 'Absolute path to the binary plan file (when command=plan and changes exist)'
95+
value: ${{ steps.run.outputs.plan_file }}
96+
fmt-result:
97+
description: 'Whether fmt found formatting differences (true/false)'
98+
value: ${{ steps.run.outputs.fmt_result }}
99+
100+
runs:
101+
using: 'composite'
102+
steps:
103+
# ── 1. Install ghoten binary ────────────────────────────────────────────
104+
- name: Setup Ghoten
105+
id: setup
106+
shell: bash
107+
run: bash "${GITHUB_ACTION_PATH}/action/setup.sh"
108+
env:
109+
INPUT_VERSION: ${{ inputs.version }}
110+
111+
# ── 2. Authenticate to GHCR ─────────────────────────────────────────────
112+
- name: Authenticate to GHCR
113+
shell: bash
114+
run: bash "${GITHUB_ACTION_PATH}/action/auth.sh"
115+
env:
116+
INPUT_GITHUB_TOKEN: ${{ inputs.github-token || github.token }}
117+
118+
# ── 3. Initialize (skip for fmt — no backend needed) ────────────────────
119+
- name: Initialize
120+
if: inputs.auto-init == 'true' && inputs.command != 'fmt'
121+
id: init
122+
shell: bash
123+
run: bash "${GITHUB_ACTION_PATH}/action/init.sh"
124+
env:
125+
INPUT_WORKING_DIRECTORY: ${{ inputs.working-directory }}
126+
INPUT_WORKSPACE: ${{ inputs.workspace }}
127+
INPUT_BACKEND_REPOSITORY: ${{ inputs.backend-repository }}
128+
INPUT_BACKEND_CONFIG: ${{ inputs.backend-config }}
129+
INPUT_INIT_ARGS: ${{ inputs.init-args }}
130+
INPUT_COMPRESSION: ${{ inputs.compression }}
131+
INPUT_LOCK_TTL: ${{ inputs.lock-ttl }}
132+
INPUT_MAX_VERSIONS: ${{ inputs.max-versions }}
133+
134+
# ── 4. Run the command ───────────────────────────────────────────────────
135+
- name: Run ghoten ${{ inputs.command }}
136+
id: run
137+
shell: bash
138+
run: bash "${GITHUB_ACTION_PATH}/action/run.sh"
139+
env:
140+
INPUT_COMMAND: ${{ inputs.command }}
141+
INPUT_WORKING_DIRECTORY: ${{ inputs.working-directory }}
142+
INPUT_VAR_FILE: ${{ inputs.var-file }}
143+
INPUT_VARIABLES: ${{ inputs.variables }}
144+
INPUT_ARGS: ${{ inputs.args }}
145+
INPUT_FMT_CHECK: ${{ inputs.fmt-check }}
146+
147+
# ── 5. PR Comment ────────────────────────────────────────────────────────
148+
- name: Comment on PR
149+
if: always() && inputs.comment-on-pr == 'true' && (github.event_name == 'pull_request' || github.event_name == 'pull_request_target')
150+
shell: bash
151+
run: bash "${GITHUB_ACTION_PATH}/action/comment.sh"
152+
env:
153+
INPUT_COMMAND: ${{ inputs.command }}
154+
INPUT_WORKING_DIRECTORY: ${{ inputs.working-directory }}
155+
INPUT_WORKSPACE: ${{ inputs.workspace }}
156+
INPUT_GITHUB_TOKEN: ${{ inputs.github-token || github.token }}
157+
PLAN_HAS_CHANGES: ${{ steps.run.outputs.plan_has_changes }}
158+
COMMAND_EXITCODE: ${{ steps.run.outputs.exitcode }}
159+
FMT_RESULT: ${{ steps.run.outputs.fmt_result }}
160+
161+
# ── 6. Job Summary ──────────────────────────────────────────────────────
162+
- name: Generate Summary
163+
if: always() && inputs.add-summary == 'true'
164+
shell: bash
165+
run: bash "${GITHUB_ACTION_PATH}/action/summary.sh"
166+
env:
167+
INPUT_COMMAND: ${{ inputs.command }}
168+
INPUT_WORKING_DIRECTORY: ${{ inputs.working-directory }}
169+
INPUT_WORKSPACE: ${{ inputs.workspace }}
170+
INPUT_BACKEND_REPOSITORY: ${{ inputs.backend-repository }}
171+
PLAN_HAS_CHANGES: ${{ steps.run.outputs.plan_has_changes }}
172+
COMMAND_EXITCODE: ${{ steps.run.outputs.exitcode }}
173+
FMT_RESULT: ${{ steps.run.outputs.fmt_result }}

action/auth.sh

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#!/usr/bin/env bash
2+
# Ghoten Action — Auth: authenticate to GHCR via Docker config
3+
set -euo pipefail
4+
5+
TOKEN="${INPUT_GITHUB_TOKEN:-}"
6+
ACTOR="${GITHUB_ACTOR:-}"
7+
8+
if [[ -z "$TOKEN" ]]; then
9+
echo "::error title=Ghoten Auth::github-token is required for GHCR authentication"
10+
exit 1
11+
fi
12+
13+
if [[ -z "$ACTOR" ]]; then
14+
echo "::error title=Ghoten Auth::GITHUB_ACTOR is not set — cannot authenticate to GHCR"
15+
exit 1
16+
fi
17+
18+
DOCKER_CONFIG="${HOME}/.docker"
19+
mkdir -p "$DOCKER_CONFIG"
20+
21+
# base64 encoding — handle GNU (with -w0) and macOS/BusyBox (without)
22+
AUTH=$(printf '%s:%s' "$ACTOR" "$TOKEN" | base64 -w0 2>/dev/null || printf '%s:%s' "$ACTOR" "$TOKEN" | base64)
23+
24+
# Merge with existing config if present, otherwise create fresh
25+
if [[ -f "$DOCKER_CONFIG/config.json" ]]; then
26+
if jq -e . "$DOCKER_CONFIG/config.json" >/dev/null 2>&1; then
27+
tmp_cfg="$(mktemp)"
28+
jq --arg auth "$AUTH" '
29+
.auths |= (. // {}) |
30+
.auths["ghcr.io"] |= (. // {}) |
31+
.auths["ghcr.io"].auth = $auth
32+
' "$DOCKER_CONFIG/config.json" > "$tmp_cfg"
33+
mv "$tmp_cfg" "$DOCKER_CONFIG/config.json"
34+
else
35+
# Existing config is not valid JSON — start fresh
36+
printf '{"auths":{"ghcr.io":{"auth":"%s"}}}' "$AUTH" > "$DOCKER_CONFIG/config.json"
37+
fi
38+
else
39+
printf '{"auths":{"ghcr.io":{"auth":"%s"}}}' "$AUTH" > "$DOCKER_CONFIG/config.json"
40+
fi
41+
42+
echo "DOCKER_CONFIG=${DOCKER_CONFIG}" >> "$GITHUB_ENV"
43+
44+
echo "🔐 Authenticated to ghcr.io as ${ACTOR}"

0 commit comments

Comments
 (0)