diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000000..1933c4ece13 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,118 @@ +# Copilot Instructions for OpenTofu ORAS Fork + +## Project Overview + +This is a **fork** of [opentofu/opentofu](https://github.com/opentofu/opentofu) that adds an **ORAS backend** for storing OpenTofu state in OCI registries (Docker, GHCR, ECR, etc.). + +## Branch Strategy + +``` +main โ†’ Synchronized with opentofu/opentofu:main (tracking only, do not commit here) +develop โ†’ Main development branch (all PRs target here) +``` + +### Branch Rules + +| Branch | Purpose | Commits | +|--------|---------|---------| +| `main` | Tracks upstream opentofu/opentofu | Only via sync-upstream workflow | +| `develop` | All fork development | Via Pull Requests | + +### Workflow + +1. **Sync upstream**: `sync-upstream.yml` runs daily and when new upstream tags are detected +2. **PR to develop**: Creates PR `๐Ÿš€ Release vX.Y.Z` from `main` โ†’ `develop` +3. **Review & merge**: Manually merge the PR (resolve conflicts if any) +4. **Auto-release**: `auto-release.yml` creates tag `vX.Y.Z-oci` and GitHub Release +5. **Build**: `release-fork.yml` builds binaries for all platforms + +## Release Naming Convention + +Fork releases follow upstream versions with `-oci` suffix: + +- Upstream: `v1.12.0` +- Fork: `v1.12.0-oci` + +This allows users to choose which upstream version they want with ORAS support. + +## Key Directories + +### ORAS Backend (main contribution) + +``` +internal/backend/remote-state/oras/ +โ”œโ”€โ”€ backend.go # Backend implementation +โ”œโ”€โ”€ client.go # OCI registry client +โ”œโ”€โ”€ state.go # State management +โ”œโ”€โ”€ locking.go # Distributed locking +โ”œโ”€โ”€ versioning.go # State versioning +โ”œโ”€โ”€ README.md # Detailed documentation +โ””โ”€โ”€ *_test.go # Tests +``` + +### Fork-specific files (not in upstream) + +``` +.github/ +โ”œโ”€โ”€ copilot-instructions.md # This file +โ”œโ”€โ”€ release.yml # Release notes configuration +โ”œโ”€โ”€ labeler.yml # PR auto-labeling rules +โ””โ”€โ”€ workflows/ + โ”œโ”€โ”€ release-fork.yml # Fork release workflow + โ”œโ”€โ”€ sync-upstream.yml # Upstream sync automation + โ”œโ”€โ”€ auto-release.yml # Auto-tagging on merge + โ””โ”€โ”€ labeler.yml # PR labeler workflow +``` + +## Development Guidelines + +### Creating PRs + +1. Always target `develop` branch +2. Use descriptive titles for release notes generation +3. Apply appropriate labels (auto-labeler will help): + - `oras`, `oci`, `backend` - ORAS backend changes + - `enhancement`, `feature` - New features + - `bug`, `fix` - Bug fixes + - `documentation` - Docs changes + - `ci` - CI/CD changes + +### Commit Messages + +No strict format required, but be descriptive. Examples: +- `Add compression support to ORAS backend` +- `Fix lock acquisition race condition` +- `Update CI workflows for develop branch` + +### Testing + +```bash +# Run ORAS backend tests +go test ./internal/backend/remote-state/oras/... + +# Run all tests +go test ./... +``` + +## Files to NEVER modify on develop + +These files should only change via upstream sync: + +- `LICENSE` +- `CHARTER.md` +- `GOVERNANCE.md` +- Core OpenTofu code (unless fixing integration with ORAS) + +## Labels for Release Notes + +PRs are automatically categorized in releases based on labels: + +| Label | Category | +|-------|----------| +| `enhancement`, `feature` | ๐Ÿš€ Features | +| `bug`, `fix` | ๐Ÿ› Bug Fixes | +| `oras`, `oci`, `backend` | ๐Ÿ“ฆ ORAS Backend | +| `security` | ๐Ÿ”’ Security | +| `documentation` | ๐Ÿ“š Documentation | +| `test` | ๐Ÿงช Tests | +| `maintenance`, `chore` | ๐Ÿ”ง Maintenance | diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000000..12884667c69 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,49 @@ +# Dependabot configuration for OpenTofu ORAS Fork +# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + # Go modules + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "06:00" + timezone: "UTC" + target-branch: "develop" + labels: + - "dependencies" + - "go" + commit-message: + prefix: "deps(go)" + open-pull-requests-limit: 10 + groups: + # Group minor and patch updates together + go-minor-patch: + patterns: + - "*" + update-types: + - "minor" + - "patch" + + # GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "06:00" + timezone: "UTC" + target-branch: "develop" + labels: + - "dependencies" + - "ci" + commit-message: + prefix: "deps(actions)" + open-pull-requests-limit: 5 + groups: + # Group all GitHub Actions updates together + github-actions: + patterns: + - "*" diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml new file mode 100644 index 00000000000..d1ba54c122a --- /dev/null +++ b/.github/workflows/auto-release.yml @@ -0,0 +1,121 @@ +name: Auto Release + +on: + pull_request: + types: [closed] + branches: + - develop + +permissions: + contents: write + +jobs: + create-release: + name: Create release on merge + if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.title, '๐Ÿš€ Release v') + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + ref: develop + + - name: Extract version from PR title + id: version + run: | + # Extract version from PR title "๐Ÿš€ Release v1.2.3" + PR_TITLE="${{ github.event.pull_request.title }}" + VERSION=$(echo "$PR_TITLE" | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+') + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "tag=${VERSION}-oci" >> $GITHUB_OUTPUT + echo "Extracted version: $VERSION" + + - name: Configure Git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Create and push tag + run: | + TAG="${{ steps.version.outputs.tag }}" + git tag -a "$TAG" -m "Release $TAG" + git push origin "$TAG" + + - name: Create GitHub Release + uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda # v2.2.1 + with: + tag_name: ${{ steps.version.outputs.tag }} + name: ${{ steps.version.outputs.tag }} + generate_release_notes: true + draft: false + prerelease: false + body: | + ## OpenTofu ${{ steps.version.outputs.version }} + ORAS Backend + + This release is based on [OpenTofu ${{ steps.version.outputs.version }}](https://github.com/opentofu/opentofu/releases/tag/${{ steps.version.outputs.version }}) with the ORAS backend for OCI registry state storage. + + ### ORAS Backend Features + + - ๐Ÿ“ฆ Store state in any OCI-compatible registry (GHCR, ECR, ACR, Docker Hub, etc.) + - ๐Ÿ”’ Distributed locking support + - ๐Ÿ“œ State versioning with configurable retention + - ๐Ÿ—œ๏ธ Optional gzip compression + - ๐Ÿ” Compatible with OpenTofu state encryption + + ### Quick Start + + ```hcl + terraform { + backend "oras" { + repository = "ghcr.io/your-org/tf-state" + } + } + ``` + + See [ORAS Backend Documentation](https://github.com/${{ github.repository }}/blob/develop/internal/backend/remote-state/oras/README.md) for full configuration options. + + --- + + **Full Changelog**: See auto-generated release notes below. + + notify-conflict: + name: Create issue on conflict + if: github.event.pull_request.merged == false && startsWith(github.event.pull_request.title, '๐Ÿš€ Release v') + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - name: Extract version from PR title + id: version + run: | + PR_TITLE="${{ github.event.pull_request.title }}" + VERSION=$(echo "$PR_TITLE" | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+') + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Create conflict issue + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + const version = '${{ steps.version.outputs.version }}'; + const prNumber = ${{ github.event.pull_request.number }}; + + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: `โš ๏ธ Release ${version} requires manual intervention`, + body: `The release PR #${prNumber} was closed without merging. + + This may indicate: + - Merge conflicts that need manual resolution + - The PR was intentionally closed + + ### Action Required + + 1. Check PR #${prNumber} for details + 2. If conflicts exist, resolve them manually + 3. Re-run the sync workflow or create a new release PR + + /cc @${context.repo.owner}`, + labels: ['release', 'needs-attention'] + }); diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 49fa634651a..692b76dd04f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,6 +5,7 @@ on: push: branches: - main + - develop - 'v[0-9]+.[0-9]+' tags: - 'v[0-9]+.[0-9]+.[0-9]+*' diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 65336b86975..a65b6e6e874 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -20,6 +20,7 @@ on: push: branches: - main + - develop - 'v[0-9]+.[0-9]+' - checks-workflow-dev/* tags: diff --git a/.github/workflows/govulncheck.yml b/.github/workflows/govulncheck.yml deleted file mode 100644 index 706b943609a..00000000000 --- a/.github/workflows/govulncheck.yml +++ /dev/null @@ -1,90 +0,0 @@ -# This workflow is meant to run govulncheck on all the branches -# that are containing a maintained version of OpenTofu. -# For more considerations about this, check this PR: https://github.com/opentofu/opentofu/pull/2600 -# -# This will try to create an issue for each vulnerability key that is found. -# If an issue for it already exists, it will skip creating it. -# -# This is meant to run _only_ from the main branch, on a scheduled manner. -# All the other branches will be scanned directly by the run triggered from the main branch. - -name: Govulncheck - -on: - schedule: - - cron: '00 15 * * MON' - workflow_dispatch: {} - -jobs: - govulncheck: - name: Run govulncheck for ${{ matrix.branch }} - runs-on: ubuntu-latest - strategy: - matrix: - include: - - { branch: main } - - { branch: v1.11 } - - { branch: v1.10 } - - { branch: v1.9 } - - { branch: v1.8 } - fail-fast: false - steps: - - name: Checkout branch to be scanned - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - ref: ${{matrix.branch}} - - - name: Install Go toolchain - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 - with: - go-version-file: 'go.mod' - - - name: Install govulncheck - run: go install golang.org/x/vuln/cmd/govulncheck@d1f380186385b4f64e00313f31743df8e4b89a77 # v1.1.4 - shell: bash - - - name: Run and report govulncheck findings - run: | - govulncheck -format json ./... | tee results - # This is parsing the output of govulncheck by: - # * extracting only the findings that are affecting the current branch (.finding | select(.trace | length > 1)) - # * getting only the vulnerability key out of the objects (.osv) - # * sorting and deduplicating the generated vulnerability keys (sort -u) - # * compacting the result into a json array like ["vulnKey1", "vulnKey2", ...] (jq -cs '.') - # * saving the results into a file which name is the version that we are scanning like "v1.8" (> "${{matrix.branch}}") - cat results | jq '.finding | select(.trace | length > 1) | .osv' | sort -u | jq -cs '.' > "${{matrix.branch}}" - shell: bash - - # Upload the artifact to make it available to the next job. - # The artifact will be named as the branch name that we are scanning ("main" or "v1.7"...) - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: ${{matrix.branch}}-results - path: ${{matrix.branch}} - - create-issues: - name: Compile results and create GH issues - needs: - - govulncheck - runs-on: ubuntu-latest - if: github.ref == 'refs/heads/main' - env: - GH_TOKEN: ${{ github.token }} - steps: - - name: Checkout branch for running the script - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - sparse-checkout: | - .github - # By providing the path where to download the artifacts and "merge-multiple: true", the downloader - # will gather all the files generated in the job(s) above into a single directory flattening the file tree. - # Eg: Instead of writing the results into "results/main-results/main" it will write the results into "results/main" - - name: Download vulns results - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4 - with: - path: results - merge-multiple: true - - name: Run and report govulncheck findings - run: .github/scripts/govulncheck-submit-issues.sh "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" - shell: bash diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index e60e9b37214..b96f3e35921 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -26,7 +26,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Label PR by size - uses: codelytv/pr-size-labeler@56f6f0fc35c7cc0f72963b8571fabd12128e252c # v1.10.1 + uses: codelytv/pr-size-labeler@v1 with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} xs_label: 'size/XS' diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml deleted file mode 100644 index bc48a75f387..00000000000 --- a/.github/workflows/nightly.yml +++ /dev/null @@ -1,110 +0,0 @@ -name: Nightly Build - -on: - schedule: - - cron: "0 1 * * *" # 1 AM UTC daily - workflow_dispatch: - -jobs: - nightly: - runs-on: larger-runners - permissions: - contents: read - id-token: write - packages: write - steps: - - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - ref: main - fetch-depth: 0 - - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version-file: "go.mod" - - # Prepare the nightly version to be able to have it returned correctly when running `tofu --version`. - - name: Prepare version file - run: | - commit_created_at=`git show --no-patch --format=%cd --date=format:%Y%m%d%H%M%S ${GITHUB_SHA}` - short_commit=`git rev-parse --short=12 ${GITHUB_SHA}` - sed -i "s/-dev/-nightly${commit_created_at}-${short_commit}/" version/VERSION - - - name: Run GoReleaser - uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0 - with: - version: v1.21.2 - distribution: goreleaser-pro - args: release --nightly --clean --timeout=60m --skip=sign,docker - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} - RELEASE_FLAG_LATEST: "false" - RELEASE_FLAG_PRERELEASE: "true" - - # The step before goreleaser one updated version/VERSION to have it stored in the binary, but we want to bring the - # repo to a clean state now. - - name: Restore version - run: git restore version/VERSION - - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: nightly-dist - path: dist - retention-days: 7 - - - name: Setup dependencies - run: sudo apt-get update && sudo apt-get install rclone jq - - - name: Prepare nightly artifacts - run: | - # Get today's date in YYYYMMDD format - DATE=$(date +%Y%m%d) - VERSION=$(grep -E '"version":' dist/metadata.json | cut -d'"' -f4) - COMMIT=$(git rev-parse --short HEAD) - - # Create a staging directory for upload - mkdir -p ./upload/nightlies/${DATE} - - # Copy relevant artifacts - cp dist/*.tar.gz ./upload/nightlies/${DATE}/ 2>/dev/null || true - cp dist/*.zip ./upload/nightlies/${DATE}/ 2>/dev/null || true - cp dist/*SHA256SUMS* ./upload/nightlies/${DATE}/ 2>/dev/null || true - - # Create latest.json - cat > ./upload/nightlies/latest.json </dev/null | xargs -n1 basename | jq -R -s -c 'split("\n")[:-1]') - } - EOF - - echo "nightly build artifacts for ${DATE} ready to upload" - echo "Version: ${VERSION}" - echo "Commit: ${COMMIT}" - - - name: Sync to R2 - run: | - set -euo pipefail - - echo "Starting upload to R2..." - echo "Files to upload:" - find ./upload -type f -name "*.tar.gz" -o -name "*.zip" -o -name "*SHA256SUMS*" -o -name "*.json" - - if ! rclone copy --verbose ./upload/ R2:${{ secrets.R2_BUCKET_NAME }}; then - echo "ERROR: Failed to upload artifacts to R2" - exit 1 - fi - - echo "Successfully uploaded nightly build artifacts to R2" - env: - RCLONE_CONFIG_R2_TYPE: s3 - RCLONE_CONFIG_R2_PROVIDER: Cloudflare - RCLONE_CONFIG_R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} - RCLONE_CONFIG_R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} - RCLONE_CONFIG_R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }} diff --git a/.github/workflows/pr-opened.yml b/.github/workflows/pr-opened.yml deleted file mode 100644 index faa88ad3b2b..00000000000 --- a/.github/workflows/pr-opened.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Pull Request Opened -permissions: - pull-requests: write - -# only trigger on pull request closed events -on: - pull_request_target: - types: [ opened ] - -jobs: - pr_open_job: - runs-on: ubuntu-latest - steps: - - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 - with: - script: | - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: "Reminder for the PR assignee: If this is a user-visible change, please update the changelog as part of the PR." - }) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 80c96eb27cb..00000000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,156 +0,0 @@ -name: release - -on: - workflow_dispatch: - inputs: - tag: - description: "Git tag (leave empty for dry run)" - required: false - latest: - description: "Release as latest?" - required: true - type: boolean - prerelease: - description: "Release as prerelease?" - required: true - type: boolean - -jobs: - release: - name: Release - runs-on: larger-runners - environment: gpg - permissions: - contents: write - id-token: write - packages: write - - steps: - - name: Set up QEMU cross build support - uses: docker/setup-qemu-action@53851d14592bedcffcf25ea515637cff71ef929a # v3.3.0 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0 - - - name: Login to Github Container Registry - uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 - if: startsWith(inputs.tag, 'v') - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - fetch-depth: 0 - ref: ${{ inputs.tag }} - - - name: Fetch tags - run: git fetch --force --tags - - - name: Compare versions - if: startsWith(inputs.tag, 'v') - run: ./.github/scripts/compare-release-version.sh - env: - TARGET_VERSION: ${{inputs.tag}} - - - name: Check if tag is on main branch or version branch - id: validate_tag - run: | - IS_TAG_ON_MAIN=$(git branch -a --contains ${{inputs.tag}} | grep -q "main" && echo true || echo false) - IS_TAG_ON_VERSION=$(git branch -a --contains ${{inputs.tag}} | grep -E "^v[0-9]+\.[0-9]+" && echo true || echo false) - echo "IS_TAG_ON_MAIN=${IS_TAG_ON_MAIN}" >> $GITHUB_OUTPUT - echo "IS_TAG_ON_VERSION=${IS_TAG_ON_VERSION}" >> $GITHUB_OUTPUT - - - name: Check if release is allowed or not - id: validate_release - run: | - if [[ "${{ inputs.prerelease }}" == "false" && "${{ steps.validate_tag.outputs.IS_TAG_ON_MAIN }}" == "true" ]]; then - echo "ERROR: Creating stable release from a tag on main is not allowed." - exit 1 - fi - - - name: Set up Go - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 - with: - go-version-file: 'go.mod' - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0 - - - name: Install cosign - uses: sigstore/cosign-installer@main - with: - cosign-release: v2.2.0 - - - name: Setup snapcraft - run: | - sudo snap install snapcraft --classic --channel=7.x/stable - - # See https://github.com/goreleaser/goreleaser/issues/1715 - mkdir -p "$HOME/.cache/snapcraft/download" - mkdir -p "$HOME/.cache/snapcraft/stage-packages" - env: - SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_TOKEN }} - - - name: Import GPG key - if: startsWith(inputs.tag, 'v') - run: | - GPG_KEY_FILE=/tmp/signing-key.gpg - echo "${{ secrets.GPG_PRIVATE_KEY }}" | base64 --decode > "${GPG_KEY_FILE}" - - echo "${{ secrets.GPG_PRIVATE_KEY }}" | base64 --decode | gpg --import - GPG_FINGERPRINT=$(gpg --list-secret-keys --keyid-format LONG | awk '/^sec/{sub(/.*\//, "", $2); print $2; exit}') - - echo "GPG_FINGERPRINT=${GPG_FINGERPRINT}" >>"${GITHUB_ENV}" - echo "GPG_KEY_FILE=${GPG_KEY_FILE}" >> "${GITHUB_ENV}" - env: - GPG_TTY: /dev/ttys000 # Set the GPG_TTY to avoid issues with pinentry - - - name: Run GoReleaser - uses: goreleaser/goreleaser-action@5742e2a039330cbb23ebf35f046f814d4c6ff811 # v5.1.0 - with: - version: v1.21.2 - distribution: goreleaser-pro - args: release --clean --timeout=60m --snapshot=${{ !startsWith(inputs.tag, 'v') }} - env: - # Note: the GPG_FINGERPRINT and GPG_KEY_FILE are defined in the task above. If they are not set, - # goreleaser won't sign the packages. - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} - SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_TOKEN }} - RELEASE_FLAG_LATEST: ${{ inputs.latest }} - RELEASE_FLAG_PRERELEASE: ${{ inputs.prerelease }} - - - name: Remove GPG key - if: always() - run: | - rm -rf ~/.gnupg - if [ -n "${GPG_KEY_FILE}" ]; then - rm -rf "${GPG_KEY_FILE}" - fi - - - name: Upload artifacts - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 - with: - name: dist - path: dist - - - name: Upload Debian packages to PackageCloud - if: startsWith(inputs.tag, 'v') && "${{ inputs.prerelease }}" != "true" - uses: computology/packagecloud-github-action@v0.6 - with: - PACKAGE-NAME: dist/*.deb - PACKAGECLOUD-USERNAME: opentofu - PACKAGECLOUD-REPONAME: tofu - PACKAGECLOUD-DISTRO: any/any - PACKAGECLOUD-TOKEN: ${{ secrets.PACKAGECLOUD_TOKEN }} - - name: Upload RPM packages to PackageCloud - if: startsWith(inputs.tag, 'v') && "${{ inputs.prerelease }}" != "true" - uses: computology/packagecloud-github-action@v0.6 - with: - PACKAGE-NAME: dist/*.rpm - PACKAGECLOUD-USERNAME: opentofu - PACKAGECLOUD-REPONAME: tofu - PACKAGECLOUD-DISTRO: rpm_any/rpm_any - PACKAGECLOUD-TOKEN: ${{ secrets.PACKAGECLOUD_TOKEN }} diff --git a/.github/workflows/sync-upstream.yml b/.github/workflows/sync-upstream.yml new file mode 100644 index 00000000000..979d32ad494 --- /dev/null +++ b/.github/workflows/sync-upstream.yml @@ -0,0 +1,135 @@ +name: Sync Upstream + +on: + schedule: + # Run daily at 6:00 UTC + - cron: '0 6 * * *' + workflow_dispatch: + inputs: + force_sync: + description: 'Force sync even without new tags' + required: false + default: 'false' + type: boolean + +permissions: + contents: write + pull-requests: write + +env: + UPSTREAM_REPO: opentofu/opentofu + UPSTREAM_BRANCH: main + +jobs: + check-upstream: + name: Check for upstream updates + runs-on: ubuntu-latest + outputs: + has_new_tag: ${{ steps.check.outputs.has_new_tag }} + latest_tag: ${{ steps.check.outputs.latest_tag }} + has_new_commits: ${{ steps.check.outputs.has_new_commits }} + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + + - name: Add upstream remote + run: | + git remote add upstream https://github.com/${{ env.UPSTREAM_REPO }}.git || true + git fetch upstream --tags + + - name: Check for updates + id: check + run: | + # Get latest upstream tag (stable releases only, no alpha/beta/rc) + LATEST_UPSTREAM_TAG=$(git tag -l 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | grep -v -E '(alpha|beta|rc)' | head -n1) + echo "Latest upstream tag: $LATEST_UPSTREAM_TAG" + echo "latest_tag=$LATEST_UPSTREAM_TAG" >> $GITHUB_OUTPUT + + # Check if we already have this tag with -oci suffix + if git tag -l "${LATEST_UPSTREAM_TAG}-oci" | grep -q .; then + echo "Tag ${LATEST_UPSTREAM_TAG}-oci already exists" + echo "has_new_tag=false" >> $GITHUB_OUTPUT + else + echo "New tag available: $LATEST_UPSTREAM_TAG" + echo "has_new_tag=true" >> $GITHUB_OUTPUT + fi + + # Check for new commits on upstream main + git fetch upstream ${{ env.UPSTREAM_BRANCH }} + LOCAL_MAIN=$(git rev-parse origin/main 2>/dev/null || echo "none") + UPSTREAM_MAIN=$(git rev-parse upstream/${{ env.UPSTREAM_BRANCH }}) + + if [ "$LOCAL_MAIN" != "$UPSTREAM_MAIN" ]; then + echo "has_new_commits=true" >> $GITHUB_OUTPUT + else + echo "has_new_commits=false" >> $GITHUB_OUTPUT + fi + + sync-main: + name: Sync main branch + needs: check-upstream + if: needs.check-upstream.outputs.has_new_commits == 'true' || github.event.inputs.force_sync == 'true' + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Configure Git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Sync main with upstream + run: | + git remote add upstream https://github.com/${{ env.UPSTREAM_REPO }}.git || true + git fetch upstream ${{ env.UPSTREAM_BRANCH }} --tags + + git checkout main + git reset --hard upstream/${{ env.UPSTREAM_BRANCH }} + git push origin main --force + + create-release-pr: + name: Create release PR + needs: [check-upstream, sync-main] + if: needs.check-upstream.outputs.has_new_tag == 'true' + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + ref: main + + - name: Create Pull Request + uses: peter-evans/create-pull-request@5e914681df9dc83aa4e4905692ca88beb2f9e91f # v7.0.5 + with: + token: ${{ secrets.GITHUB_TOKEN }} + branch: release/${{ needs.check-upstream.outputs.latest_tag }} + base: develop + title: "๐Ÿš€ Release ${{ needs.check-upstream.outputs.latest_tag }}" + body: | + ## Upstream Release + + This PR syncs with upstream OpenTofu release **${{ needs.check-upstream.outputs.latest_tag }}**. + + ### Upstream Release Notes + See: https://github.com/${{ env.UPSTREAM_REPO }}/releases/tag/${{ needs.check-upstream.outputs.latest_tag }} + + ### After Merging + + When this PR is merged, a new release `${{ needs.check-upstream.outputs.latest_tag }}-oci` will be created automatically. + + ### Checklist + + - [ ] Review changes for conflicts with ORAS backend + - [ ] Ensure tests pass + - [ ] Resolve any merge conflicts + labels: | + release + upstream-sync + draft: false diff --git a/.github/workflows/update-top-issues-ranking.yml b/.github/workflows/update-top-issues-ranking.yml deleted file mode 100644 index 05d262dbe2e..00000000000 --- a/.github/workflows/update-top-issues-ranking.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: update-top-issues-ranking - -on: - workflow_dispatch: - schedule: - - cron: '0 10 * * *' - -jobs: - update: - runs-on: ubuntu-latest - if: github.repository_owner == 'opentofu' - permissions: - contents: read - issues: write - steps: - - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: Setup Go - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 - with: - go-version: 1.22 - - name: Update top issues ranking - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - cd .github/scripts/update_top_issues_ranking - go mod download - go run main.go opentofu opentofu 1496 diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml deleted file mode 100644 index b56e3e4a04c..00000000000 --- a/.github/workflows/website.yml +++ /dev/null @@ -1,68 +0,0 @@ -name: Website checks - -on: - pull_request: - push: - branches: - - main - - 'v[0-9]+.[0-9]+' - tags: - - 'v[0-9]+.[0-9]+.[0-9]+*' - -jobs: - fileschanged: - name: List files changed for pull request - runs-on: ubuntu-latest - steps: - - name: "Fetch source code" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - id: diff - run: | - echo "Comparing current commit to base ref origin/${{github.event.pull_request.base.ref}}" - git fetch --no-tags --prune --no-recurse-submodules --depth=1 origin ${{github.event.pull_request.base.ref}} - echo "install=$(git diff --name-only origin/${{github.event.pull_request.base.ref}} | grep 'website/docs/intro/install' | wc -l)" | tee -a "$GITHUB_OUTPUT" - echo "website=$(git diff --name-only origin/${{github.event.pull_request.base.ref}} | grep 'website/' | wc -l)" | tee -a "$GITHUB_OUTPUT" - outputs: - install: ${{ steps.diff.outputs.install }} - website: ${{ steps.diff.outputs.website }} - - build: - name: Build - runs-on: ubuntu-latest - needs: fileschanged - if: ${{ needs.fileschanged.outputs.website != 0}} - steps: - - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - fetch-depth: 0 - - name: Prepare website container - run: docker compose -f docker-compose.build.yml build - working-directory: website - # The segregation between the next 2 steps is because on non-main branches, there are - # blog posts that point to the 'main' branch documentation that is fully missing from - # the branches that are used to maintain older versions of OpenTofu. - # Therefore, we run the full website check on main (and any branch in a PR that points - # to the main branch) and we run a trimmed down website checks for any runs against - # a non-main branch (or a PR that points to a non-main branch) - - name: Build website - if: github.ref == 'refs/heads/main' || github.base_ref == 'main' - run: docker compose -f docker-compose.build.yml up --exit-code-from website - working-directory: website - - name: Build website with warns - if: github.ref != 'refs/heads/main' && github.base_ref != 'main' - run: docker compose -f docker-compose.build-non-main.yml up --exit-code-from website - working-directory: website - - installation-instructions: - name: "Test Installation Instructions" - runs-on: ubuntu-latest - needs: fileschanged - if: ${{ needs.fileschanged.outputs.install != 0}} - - steps: - - name: "Fetch source code" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - - name: "Run Installation Instructions Test" - run: make test-linux-install-instructions diff --git a/README.md b/README.md index eea26d8a09e..30fe1cae8a4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,159 @@ -# OpenTofu +# OpenTofu + ORAS Backend + +> ๐Ÿด **This is a fork of [opentofu/opentofu](https://github.com/opentofu/opentofu)** that adds an **ORAS backend** for storing OpenTofu state in OCI registries. + +[![Release](https://img.shields.io/github/v/release/vmvarela/opentofu?label=Latest%20Release&style=flat-square)](https://github.com/vmvarela/opentofu/releases/latest) +[![OpenTofu Base](https://img.shields.io/badge/Based%20on-OpenTofu-blue?style=flat-square)](https://github.com/opentofu/opentofu) + +--- + +## ๐Ÿค– Why This Fork Exists + +This fork is maintained **independently** because: + +1. **AI-Generated Code**: The ORAS backend implementation was developed with AI assistance (GitHub Copilot). The upstream OpenTofu project has a [strict policy against AI-generated code](https://github.com/opentofu/opentofu/blob/main/CONTRIBUTING.md) due to licensing concerns with Terraform's BSL license. + +2. **Experimental Backend**: OpenTofu historically avoids adding new remote state backends to the core project. This fork serves as a reference implementation and a usable solution for those who want OCI registry state storage. + +This fork stays synchronized with upstream releases, allowing you to benefit from all OpenTofu improvements while having access to the ORAS backend. + +--- + +## ๐Ÿ“ฆ What This Fork Adds + +This fork includes an **ORAS backend** that allows you to store OpenTofu state in any OCI-compatible container registry (GitHub Container Registry, Amazon ECR, Azure ACR, Google GCR, Docker Hub, Harbor, etc.). + +### Key Features + +| Feature | Description | +|---------|-------------| +| **OCI Registry Storage** | Store state as OCI artifacts in your existing container registry | +| **Reuse Existing Auth** | Uses Docker credentials and `tofu login` tokens | +| **Distributed Locking** | Lock state to prevent concurrent modifications | +| **State Versioning** | Keep history of state versions with configurable retention | +| **Compression** | Optional gzip compression for state files | +| **Encryption Compatible** | Works with OpenTofu's client-side state encryption | + +### Quick Start + +```hcl +terraform { + backend "oras" { + repository = "ghcr.io/your-org/tf-state" + } +} +``` + +### Full Example (with versioning + encryption) + +```hcl +terraform { + backend "oras" { + repository = "ghcr.io/your-org/tf-state" + compression = "gzip" + + versioning { + enabled = true + max_versions = 10 + } + } + + encryption { + key_provider "pbkdf2" "main" { + passphrase = var.state_passphrase + } + method "aes_gcm" "main" { + key_provider = key_provider.pbkdf2.main + } + state { + method = method.aes_gcm.main + } + } +} +``` + +### ๐Ÿ“š Full Documentation + +See the [ORAS Backend README](internal/backend/remote-state/oras/README.md) for complete documentation including: +- All configuration parameters +- Authentication setup +- Locking behavior +- Versioning and retention +- Troubleshooting + +--- + +## ๐Ÿ”„ Release Versioning + +This fork follows OpenTofu releases with an `-oci` suffix: + +| OpenTofu Release | This Fork | +|------------------|-----------| +| `v1.12.0` | `v1.12.0-oci` | +| `v1.11.1` | `v1.11.1-oci` | + +This allows you to choose which OpenTofu version you want with ORAS support. + +--- + +## ๐Ÿ“ฅ Installation + +### Quick Install (Linux/macOS) + +```bash +curl -sSL https://raw.githubusercontent.com/vmvarela/opentofu/develop/install.sh | sh +``` + +### Quick Install (Windows PowerShell) + +```powershell +irm https://raw.githubusercontent.com/vmvarela/opentofu/develop/install.ps1 | iex +``` + +This installs the binary as `tofu-oras` to avoid conflicts with the official `tofu` installation. + +#### Installation Options + +**Linux/macOS:** +```bash +# Install specific version +TOFU_ORAS_VERSION=v1.12.0-oci curl -sSL https://raw.githubusercontent.com/vmvarela/opentofu/develop/install.sh | sh + +# Install to custom directory +TOFU_ORAS_INSTALL_DIR=~/.local/bin curl -sSL https://raw.githubusercontent.com/vmvarela/opentofu/develop/install.sh | sh + +# Install with custom binary name +TOFU_ORAS_BINARY_NAME=tofu curl -sSL https://raw.githubusercontent.com/vmvarela/opentofu/develop/install.sh | sh +``` + +**Windows PowerShell:** +```powershell +# Install specific version +$env:TOFU_ORAS_VERSION = "v1.12.0-oci" +irm https://raw.githubusercontent.com/vmvarela/opentofu/develop/install.ps1 | iex + +# Install to custom directory +$env:TOFU_ORAS_INSTALL_DIR = "$env:USERPROFILE\.local\bin" +irm https://raw.githubusercontent.com/vmvarela/opentofu/develop/install.ps1 | iex +``` + +### Manual Download + +Download the binary for your platform from the [Releases](https://github.com/vmvarela/opentofu/releases) page. + +### Build from Source + +```bash +git clone https://github.com/vmvarela/opentofu.git +cd opentofu +go build -o tofu-oras ./cmd/tofu +``` + +--- + +# OpenTofu (Original Project) + +> The following is the original OpenTofu README. - [HomePage](https://opentofu.org/) - [How to install](https://opentofu.org/docs/intro/install) diff --git a/RELEASE-FORK.md b/RELEASE-FORK.md deleted file mode 100644 index d113c35a905..00000000000 --- a/RELEASE-FORK.md +++ /dev/null @@ -1,256 +0,0 @@ -# OCI Fork Release Infrastructure - -## Overview - -This document describes the release infrastructure for the OpenTofu OCI fork. - -## Branch Structure - -``` -โ”œโ”€โ”€ main -โ”‚ โ”œโ”€ Synced with upstream/main -โ”‚ โ”œโ”€ No permanent workflows here -โ”‚ โ””โ”€ Used for base tracking only -โ”‚ -โ”œโ”€โ”€ backend/oci -โ”‚ โ”œโ”€ The actual backend implementation PR -โ”‚ โ”œโ”€ Rebased on latest upstream -โ”‚ โ””โ”€ Target for upstream pull request -โ”‚ -โ””โ”€โ”€ oci-releases (permanent release branch) - โ”œโ”€ Base: backend/oci + latest upstream version - โ”œโ”€ Contains: release-fork.yml workflow - โ”œโ”€ Source of: version tags (v*.*.*-oci) - โ””โ”€ Generates: GitHub releases with assets -``` - -## Release Workflow - -### Automated Process - -When you push a tag `v*.*.*-oci` to `oci-releases`, GitHub Actions: - -1. **Disk space preparation** - Frees up 15+ GB on runner -2. **Parallel builds** - Builds multiple platform variants in a matrix - - Linux: amd64, arm64, arm, 386 - - macOS: amd64, arm64 - - Windows: amd64, 386 - - FreeBSD: amd64, arm, 386 - - OpenBSD: amd64, 386 - - Solaris: amd64 -3. **Artifact collection** - Downloads all binaries -4. **Release creation** - Creates GitHub release with: - - All platform binaries - - SHA256SUMS file - - Detailed release notes - - Installation instructions - -### Creating a Release - -#### Quick Method (Recommended) - -```bash -./create-oci-release.sh v1.12.0 -``` - -The script will: -- โœ“ Sync main with upstream -- โœ“ Update oci-releases branch -- โœ“ Merge upstream version -- โœ“ Create tag v1.12.0-oci -- โœ“ Push to GitHub -- โœ“ Trigger GitHub Actions build - -#### Manual Method - -```bash -# Fetch upstream tags -git fetch upstream --tags - -# Switch to oci-releases -git checkout oci-releases - -# Merge upstream version -git merge v1.12.0 --no-ff -m "Merge upstream v1.12.0" - -# Create release tag -git tag -a v1.12.0-oci -m "Release v1.12.0 with OCI backend" - -# Push (triggers GitHub Actions) -git push origin oci-releases v1.12.0-oci -``` - -## Script Options - -### create-oci-release.sh - -```bash -# Normal release -./create-oci-release.sh v1.12.0 - -# Without pushing (test locally) -./create-oci-release.sh v1.12.0 --no-push - -# Force push if tag exists -./create-oci-release.sh v1.12.0 --force - -# Combine options -./create-oci-release.sh v1.12.0 --force --no-push -``` - -## Workflow Features - -### release-fork.yml - -**Triggers:** Push of tag matching `v*-oci` - -**Jobs:** - -1. **build** - Parallel matrix jobs - - Each builds one platform variant - - Uploads artifacts with 1-day retention -2. **create-release** - Create GitHub release - - Downloads all artifacts - - Generates SHA256SUMS - - Creates release with full notes - -**Optimization strategies:** - -- Remove dotnet, Android SDK, CodeQL -- Clean Docker images -- Clean apt cache -- Per-platform builds (no monolithic build) - -## Troubleshooting - -### Merge Conflicts - -If `merge upstream` fails: - -```bash -git checkout oci-releases -git merge v1.12.0 - -# Resolve conflicts in your editor -git add . -git commit -m "Merge upstream v1.12.0" -git tag -a v1.12.0-oci -m "Release v1.12.0 with OCI backend" -git push origin oci-releases v1.12.0-oci -``` - -### Build Failures - -Check the Actions log: -``` -https://github.com/vmvarela/opentofu/actions -``` - -Common issues: -- **Disk space** - Workflow handles cleanup, but can still fail on large builds -- **Platform-specific** - Some platforms may need special handling -- **Credential issues** - Verify GitHub token permissions - -### Disk Space Issues - -If `ubuntu-latest` still runs out of space: - -1. Reduce matrix (e.g., remove 386 and arm) -2. Split into multiple workflows by platform -3. Use self-hosted runner with more storage - -Current setup should handle all 8 platforms within 20GB limit. - -## Release Contents - -Each release includes: - -``` -tofu_linux_amd64 # Linux AMD64 binary -tofu_linux_amd64.sha256 # Checksum -tofu_linux_arm64 # Linux ARM64 binary -tofu_linux_arm64.sha256 # Checksum -tofu_linux_arm # Linux ARM binary -tofu_linux_arm.sha256 # Checksum -tofu_linux_386 # Linux 386 binary -tofu_linux_386.sha256 # Checksum -tofu_darwin_amd64 # macOS Intel binary -tofu_darwin_amd64.sha256 # Checksum -tofu_darwin_arm64 # macOS ARM binary -tofu_darwin_arm64.sha256 # Checksum -tofu_windows_amd64.exe # Windows AMD64 binary -tofu_windows_amd64.exe.sha256 # Checksum -tofu_windows_386.exe # Windows 386 binary -tofu_windows_386.exe.sha256 # Checksum -tofu_freebsd_amd64 # FreeBSD AMD64 binary -tofu_freebsd_amd64.sha256 # Checksum -tofu_freebsd_arm # FreeBSD ARM binary -tofu_freebsd_arm.sha256 # Checksum -tofu_freebsd_386 # FreeBSD 386 binary -tofu_freebsd_386.sha256 # Checksum -tofu_openbsd_amd64 # OpenBSD AMD64 binary -tofu_openbsd_amd64.sha256 # Checksum -tofu_openbsd_386 # OpenBSD 386 binary -tofu_openbsd_386.sha256 # Checksum -tofu_solaris_amd64 # Solaris AMD64 binary -tofu_solaris_amd64.sha256 # Checksum -SHA256SUMS # All checksums -``` - -## Verifying Releases - -```bash -# Download release and checksums -wget https://github.com/vmvarela/opentofu/releases/download/v1.12.0-oci/tofu_linux_amd64 -wget https://github.com/vmvarela/opentofu/releases/download/v1.12.0-oci/SHA256SUMS - -# Verify -sha256sum -c SHA256SUMS -``` - -## Maintenance - -### Branch Maintenance - -The `oci-releases` branch should: -- Always have latest upstream version merged -- Include all OCI backend commits -- Have release workflow setup - -### Tag Cleanup - -If you need to delete a release: - -```bash -# Delete local tag -git tag -d v1.12.0-oci - -# Delete remote tag -git push origin --delete v1.12.0-oci - -# Delete GitHub release (via web UI) -``` - -## Integration with Upstream - -When you update `backend/oci` with fixes: - -1. Push to `backend/oci` (for PR) -2. Manually update `oci-releases`: - ```bash - git checkout oci-releases - git merge backend/oci - git push origin oci-releases - ``` - -3. Next release will include those fixes - -## Future Enhancements - -Potential improvements: - -- [ ] Automated upstream detection (email/webhook) -- [ ] Automatic PR creation on new upstream release -- [ ] Checksum signing with GPG -- [ ] SBOM generation -- [ ] Docker image builds -- [ ] Brew formula updates diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 00000000000..75dd61c70c4 --- /dev/null +++ b/install.ps1 @@ -0,0 +1,139 @@ +# OpenTofu ORAS Fork Installer for Windows +# Usage: irm https://raw.githubusercontent.com/vmvarela/opentofu/develop/install.ps1 | iex +# +# Or with options: +# $env:TOFU_ORAS_VERSION = "v1.12.0-oci" +# $env:TOFU_ORAS_INSTALL_DIR = "$env:USERPROFILE\.local\bin" +# irm https://raw.githubusercontent.com/vmvarela/opentofu/develop/install.ps1 | iex + +$ErrorActionPreference = "Stop" + +$GitHubRepo = "vmvarela/opentofu" +$BinaryName = if ($env:TOFU_ORAS_BINARY_NAME) { $env:TOFU_ORAS_BINARY_NAME } else { "tofu-oras" } +$InstallDir = if ($env:TOFU_ORAS_INSTALL_DIR) { $env:TOFU_ORAS_INSTALL_DIR } else { "$env:LOCALAPPDATA\Programs\tofu-oras" } + +function Write-Info { + param([string]$Message) + Write-Host "[INFO] " -ForegroundColor Green -NoNewline + Write-Host $Message +} + +function Write-Warn { + param([string]$Message) + Write-Host "[WARN] " -ForegroundColor Yellow -NoNewline + Write-Host $Message +} + +function Write-Err { + param([string]$Message) + Write-Host "[ERROR] " -ForegroundColor Red -NoNewline + Write-Host $Message + exit 1 +} + +function Get-Architecture { + $arch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture + switch ($arch) { + "X64" { return "amd64" } + "X86" { return "386" } + "Arm64" { return "arm64" } + default { Write-Err "Unsupported architecture: $arch" } + } +} + +function Get-LatestVersion { + try { + $response = Invoke-RestMethod -Uri "https://api.github.com/repos/$GitHubRepo/releases/latest" -UseBasicParsing + return $response.tag_name + } + catch { + Write-Err "Failed to fetch latest version: $_" + } +} + +function Add-ToPath { + param([string]$Directory) + + $currentPath = [Environment]::GetEnvironmentVariable("Path", "User") + if ($currentPath -notlike "*$Directory*") { + [Environment]::SetEnvironmentVariable("Path", "$currentPath;$Directory", "User") + $env:Path = "$env:Path;$Directory" + Write-Info "Added $Directory to PATH" + return $true + } + return $false +} + +# Main +Write-Host "" +Write-Info "OpenTofu ORAS Fork Installer for Windows" +Write-Host "" + +# Detect architecture +$Arch = Get-Architecture +Write-Info "Detected architecture: windows/$Arch" + +# Get version +$Version = if ($env:TOFU_ORAS_VERSION) { $env:TOFU_ORAS_VERSION } else { $null } +if (-not $Version) { + Write-Info "Fetching latest version..." + $Version = Get-LatestVersion +} +Write-Info "Version: $Version" + +# Build download URL +$ArtifactName = "tofu_windows_${Arch}.exe" +$DownloadUrl = "https://github.com/$GitHubRepo/releases/download/$Version/$ArtifactName" +Write-Info "Downloading from: $DownloadUrl" + +# Create install directory +if (-not (Test-Path $InstallDir)) { + New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null + Write-Info "Created directory: $InstallDir" +} + +# Download +$TempFile = Join-Path $env:TEMP $ArtifactName +try { + Invoke-WebRequest -Uri $DownloadUrl -OutFile $TempFile -UseBasicParsing +} +catch { + Write-Err "Download failed: $_" +} + +# Install +$InstallPath = Join-Path $InstallDir "$BinaryName.exe" +Move-Item -Path $TempFile -Destination $InstallPath -Force +Write-Info "Installed to: $InstallPath" + +# Add to PATH +$PathAdded = Add-ToPath -Directory $InstallDir + +# Verify +Write-Host "" +Write-Info "โœ… Installation complete!" +Write-Host "" +Write-Info "Binary installed: $InstallPath" + +try { + $versionOutput = & $InstallPath version 2>&1 + Write-Info "Version: $versionOutput" +} +catch { + Write-Err "Failed to retrieve version from installed binary: $_" + Write-Info "Version (expected): $Version" +} + +Write-Host "" +Write-Info "Usage:" +Write-Host " $BinaryName init" +Write-Host " $BinaryName plan" +Write-Host " $BinaryName apply" +Write-Host "" + +if ($PathAdded) { + Write-Warn "Please restart your terminal for PATH changes to take effect." + Write-Host "" +} + +Write-Info "Documentation: https://github.com/$GitHubRepo/blob/develop/internal/backend/remote-state/oras/README.md" diff --git a/install.sh b/install.sh new file mode 100755 index 00000000000..7073691bbdb --- /dev/null +++ b/install.sh @@ -0,0 +1,202 @@ +#!/bin/sh +# OpenTofu ORAS Fork Installer +# Usage: curl -sSL https://raw.githubusercontent.com/vmvarela/opentofu/develop/install.sh | sh +# +# Environment variables: +# TOFU_ORAS_VERSION - Specific version to install (e.g., v1.12.0-oci). Default: latest +# TOFU_ORAS_INSTALL_DIR - Installation directory. Default: /usr/local/bin +# TOFU_ORAS_BINARY_NAME - Binary name. Default: tofu-oras + +set -e + +GITHUB_REPO="vmvarela/opentofu" +BINARY_NAME="${TOFU_ORAS_BINARY_NAME:-tofu-oras}" +INSTALL_DIR="${TOFU_ORAS_INSTALL_DIR:-/usr/local/bin}" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +info() { + printf "${GREEN}[INFO]${NC} %s\n" "$1" +} + +warn() { + printf "${YELLOW}[WARN]${NC} %s\n" "$1" +} + +error() { + printf "${RED}[ERROR]${NC} %s\n" "$1" + exit 1 +} + +# Detect OS +detect_os() { + OS="$(uname -s | tr '[:upper:]' '[:lower:]')" + case "$OS" in + linux*) OS="linux" ;; + darwin*) OS="darwin" ;; + freebsd*) OS="freebsd" ;; + openbsd*) OS="openbsd" ;; + mingw*|msys*|cygwin*) OS="windows" ;; + *) error "Unsupported operating system: $OS" ;; + esac + echo "$OS" +} + +# Detect architecture +detect_arch() { + ARCH="$(uname -m)" + case "$ARCH" in + x86_64|amd64) ARCH="amd64" ;; + aarch64|arm64) ARCH="arm64" ;; + armv7l|armv6l) ARCH="arm" ;; + i386|i686) ARCH="386" ;; + *) error "Unsupported architecture: $ARCH" ;; + esac + echo "$ARCH" +} + +# Get latest version from GitHub API +get_latest_version() { + if command -v curl >/dev/null 2>&1; then + curl -sSL "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/' + elif command -v wget >/dev/null 2>&1; then + wget -qO- "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/' + else + error "Neither curl nor wget found. Please install one of them." + fi +} + +# Download file +download() { + URL="$1" + OUTPUT="$2" + + if command -v curl >/dev/null 2>&1; then + curl -sSL "$URL" -o "$OUTPUT" + elif command -v wget >/dev/null 2>&1; then + wget -q "$URL" -O "$OUTPUT" + else + error "Neither curl nor wget found. Please install one of them." + fi +} + +# Main installation +main() { + info "OpenTofu ORAS Fork Installer" + echo "" + + # Detect platform + OS=$(detect_os) + ARCH=$(detect_arch) + info "Detected platform: ${OS}/${ARCH}" + + # Get version + VERSION="${TOFU_ORAS_VERSION:-}" + if [ -z "$VERSION" ]; then + info "Fetching latest version..." + VERSION=$(get_latest_version) + if [ -z "$VERSION" ]; then + error "Could not determine latest version. Please set TOFU_ORAS_VERSION environment variable." + fi + fi + info "Version: ${VERSION}" + + # Build download URL + BINARY_SUFFIX="" + if [ "$OS" = "windows" ]; then + BINARY_SUFFIX=".exe" + fi + + ARTIFACT_NAME="tofu_${OS}_${ARCH}${BINARY_SUFFIX}" + DOWNLOAD_URL="https://github.com/${GITHUB_REPO}/releases/download/${VERSION}/${ARTIFACT_NAME}" + + info "Downloading from: ${DOWNLOAD_URL}" + + # Create temp directory + TMP_DIR=$(mktemp -d) + trap "rm -rf $TMP_DIR" EXIT + + TMP_FILE="${TMP_DIR}/${ARTIFACT_NAME}" + + # Download + if ! download "$DOWNLOAD_URL" "$TMP_FILE"; then + # Download binary + download "$DOWNLOAD_URL" "$TMP_FILE" + + if [ ! -f "$TMP_FILE" ]; then + error "Download failed" + fi + + # Download and verify checksum + CHECKSUMS_URL="https://github.com/${GITHUB_REPO}/releases/download/${VERSION}/SHA256SUMS" + CHECKSUMS_FILE="${TMP_DIR}/SHA256SUMS" + + info "Downloading checksums from: ${CHECKSUMS_URL}" + download "$CHECKSUMS_URL" "$CHECKSUMS_FILE" + + if [ ! -f "$CHECKSUMS_FILE" ]; then + error "Failed to download checksum file" + fi + + EXPECTED_SUM=$(grep " ${ARTIFACT_NAME}\$" "$CHECKSUMS_FILE" | awk '{print $1}') + if [ -z "$EXPECTED_SUM" ]; then + error "No checksum entry found for ${ARTIFACT_NAME} in SHA256SUMS" + fi + + if command -v sha256sum >/dev/null 2>&1; then + ACTUAL_SUM=$(sha256sum "$TMP_FILE" | awk '{print $1}') + elif command -v shasum >/dev/null 2>&1; then + ACTUAL_SUM=$(shasum -a 256 "$TMP_FILE" | awk '{print $1}') + else + error "Neither sha256sum nor shasum is available; cannot verify checksum" + fi + + if [ "$EXPECTED_SUM" != "$ACTUAL_SUM" ]; then + error "Checksum verification failed for ${ARTIFACT_NAME}" + fi + + # Make executable + chmod +x "$TMP_FILE" + + # Install + INSTALL_PATH="${INSTALL_DIR}/${BINARY_NAME}${BINARY_SUFFIX}" + + info "Installing to: ${INSTALL_PATH}" + + # Check if we need sudo + if [ -w "$INSTALL_DIR" ]; then + mv "$TMP_FILE" "$INSTALL_PATH" + else + warn "Need elevated permissions to install to ${INSTALL_DIR}" + if command -v sudo >/dev/null 2>&1; then + sudo mv "$TMP_FILE" "$INSTALL_PATH" + sudo chmod +x "$INSTALL_PATH" + else + error "Cannot write to ${INSTALL_DIR} and sudo is not available. Try setting TOFU_ORAS_INSTALL_DIR to a writable directory." + fi + fi + + # Verify installation + if [ -x "$INSTALL_PATH" ]; then + echo "" + info "โœ… Installation complete!" + echo "" + info "Binary installed: ${INSTALL_PATH}" + info "Version: $(${INSTALL_PATH} version 2>/dev/null || echo "${VERSION}")" + echo "" + info "Usage:" + echo " ${BINARY_NAME} init" + echo " ${BINARY_NAME} plan" + echo " ${BINARY_NAME} apply" + echo "" + info "Documentation: https://github.com/${GITHUB_REPO}/blob/develop/internal/backend/remote-state/oras/README.md" + else + error "Installation verification failed" + fi +} + +main "$@"