diff --git a/.github/workflows/docs-build.yml b/.github/workflows/docs-build.yml new file mode 100644 index 0000000000..bd20286e62 --- /dev/null +++ b/.github/workflows/docs-build.yml @@ -0,0 +1,43 @@ +name: Docs build +on: + push: + branches: [main] + paths: + - "docs/**" + pull_request: + branches: [main] + paths: + - "docs/**" + release: + types: [published] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./docs + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run npm install + run: npm ci + + - name: Check formatting + run: npm run format + + - name: Run build + run: npm run build + + - name: Upload build output + uses: actions/upload-artifact@v4 + with: + name: docs-build-output + path: docs/build/ + retention-days: 1 diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml new file mode 100644 index 0000000000..f21b4388c2 --- /dev/null +++ b/.github/workflows/docs-deploy.yml @@ -0,0 +1,158 @@ +name: Docs deploy +on: + workflow_run: + workflows: ["Docs build"] + types: + - completed + +jobs: + checks: + runs-on: ubuntu-latest + outputs: + parameters: ${{ steps.parameters.outputs.result }} + steps: + - if: ${{ github.event.workflow_run.conclusion == 'failure' }} + run: echo 'The triggering workflow failed' && exit 1 + + - name: Determine deploy parameters + id: parameters + uses: actions/github-script@v7 + with: + script: | + const eventType = context.payload.workflow_run.event; + const isFork = context.payload.workflow_run.repository.fork; + + let parameters; + + console.log({eventType, isFork}); + + if (eventType == "push") { + const branch = context.payload.workflow_run.head_branch; + console.log({branch}); + const shouldDeploy = !isFork && branch == "main"; + parameters = { + event: "branch", + name: "main", + shouldDeploy + }; + } else if (eventType == "pull_request") { + const pull_number = context.payload.workflow_run.pull_requests[0].number; + const {data: pr} = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number + }); + const isApproved = pr.labels.some((l) => l.name == "preview-docs"); + + console.log({pull_number, isApproved}); + + parameters = { + event: "pr", + name: `pr-${pull_number}`, + shouldDeploy: !isFork || isApproved + }; + } else if (eventType == "release") { + parameters = { + event: "release", + name: context.payload.workflow_run.head_branch, + shouldDeploy: !isFork + }; + } + + console.log(parameters); + return parameters; + + deploy: + runs-on: ubuntu-latest + needs: checks + if: ${{ fromJson(needs.checks.outputs.parameters).shouldDeploy }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Load parameters + id: parameters + uses: actions/github-script@v7 + with: + script: | + const json = `${{ needs.checks.outputs.parameters }}`; + const parameters = JSON.parse(json); + core.setOutput("event", parameters.event); + core.setOutput("name", parameters.name); + core.setOutput("shouldDeploy", parameters.shouldDeploy); + + - run: | + echo "Starting docs deployment for ${{ steps.parameters.outputs.event }} ${{ steps.parameters.outputs.name }}" + + - name: Download artifact + uses: actions/github-script@v7 + with: + script: | + let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.payload.workflow_run.id, + }); + let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => { + return artifact.name == "docs-build-output" + })[0]; + let download = await github.rest.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + archive_format: 'zip', + }); + let fs = require('fs'); + fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/docs-build-output.zip`, Buffer.from(download.data)); + + - name: Unzip artifact + run: unzip "${{ github.workspace }}/docs-build-output.zip" -d "${{ github.workspace }}/docs/build" + + - name: Deploy Docs Subdomain + env: + TF_VAR_prefix_name: ${{ steps.parameters.outputs.name}} + TF_VAR_prefix_event_type: ${{ steps.parameters.outputs.event }} + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }} + uses: gruntwork-io/terragrunt-action@v2 + with: + tg_version: "0.58.12" + tofu_version: "1.7.1" + tg_dir: "deployment/modules/cloudflare/docs" + tg_command: "apply" + + - name: Publish to Cloudflare Pages + uses: cloudflare/pages-action@v1 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN_PAGES_UPLOAD }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + projectName: "immich-app" + workingDirectory: "docs" + directory: "build" + branch: ${{ steps.parameters.outputs.name }} + wranglerVersion: '3' + + - name: Deploy Docs Release Domain + if: ${{ steps.parameters.outputs.event == 'release' }} + env: + TF_VAR_prefix_name: ${{ steps.parameters.outputs.name}} + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }} + uses: gruntwork-io/terragrunt-action@v2 + with: + tg_version: '0.58.12' + tofu_version: '1.7.1' + tg_dir: 'deployment/modules/cloudflare/docs-release' + tg_command: 'apply' + + - name: Comment + uses: actions-cool/maintain-one-comment@v3 + if: ${{ steps.parameters.outputs.event == 'pr' }} + with: + number: ${{ github.event.workflow_run.pull_requests[0].number }} + body: | + 📖 Documentation deployed to [${{ steps.parameters.outputs.name }}.preview.immich.app](https://${{ steps.parameters.outputs.name }}.preview.immich.app) + emojis: 'rocket' + body-include: '' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3d556e5e1d..8ff2b65af4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,28 +10,6 @@ concurrency: cancel-in-progress: true jobs: - doc-tests: - name: Docs - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./docs - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Run npm install - run: npm ci - - - name: Run formatter - run: npm run format - if: ${{ !cancelled() }} - - - name: Run build - run: npm run build - if: ${{ !cancelled() }} - server-unit-tests: name: Server runs-on: ubuntu-latest diff --git a/deployment/.gitignore b/deployment/.gitignore new file mode 100644 index 0000000000..653d60a3f1 --- /dev/null +++ b/deployment/.gitignore @@ -0,0 +1,38 @@ +# OpenTofu + +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log +crash.*.log + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Include override files you do wish to add to version control using negated pattern +# !example_override.tf + +# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan +# example: *tfplan* + +# Ignore CLI configuration files +.terraformrc +terraform.rc + +# Terragrunt + +# terragrunt cache directories +**/.terragrunt-cache/* + +# Terragrunt debug output file (when using `--terragrunt-debug` option) +# See: https://terragrunt.gruntwork.io/docs/reference/cli-options/#terragrunt-debug +terragrunt-debug.tfvars.json diff --git a/deployment/modules/cloudflare/docs-release/.terraform.lock.hcl b/deployment/modules/cloudflare/docs-release/.terraform.lock.hcl new file mode 100644 index 0000000000..91dcc1a19d --- /dev/null +++ b/deployment/modules/cloudflare/docs-release/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "tofu init". +# Manual edits may be lost in future updates. + +provider "registry.opentofu.org/cloudflare/cloudflare" { + version = "4.33.0" + constraints = "4.33.0" + hashes = [ + "h1:jfvnxO1kjxUquizrBlswsytWQeHXjvNz6IZwPTuixJ4=", + "zh:1839d03c8c30d3eac4f18c78c5c095d44348eb06cc9d758136d16517094b64e3", + "zh:1fd94992cbb8ce2943cf4fe3deff01b9b1a0e913d2cdd484ceb96a859d46dc5d", + "zh:2b4f37c16a7f6d3712b03980b52b149d4ef6a544917bfe1d1c2ca2d40468daa5", + "zh:32ecb8017be0a34f72b9b0fcd43d944b99cdc903c79892a68b48719824fb194c", + "zh:6468dee137fa07b0b43f742cfdaabb6620883b00773af370e10755ba579eb7f7", + "zh:766504de95a418fd763d9474f39fb147053201d6c4efa3efa456fb39a559b28b", + "zh:800342078c0c04a36cb2558d0c5c6bf050a8b4c231abecac59e56c9868b9fa7e", + "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", + "zh:94420269672dc5bef37e1a8efab656ada5c2d6f162b52cc59ea74cf0cf35d633", + "zh:972481b86636f00771c2b5f3e408ffd9b66d42942645c8b8e11c4f4bf52285f1", + "zh:ac52d6698fa8b2a8ab820ffb59381da27684a205f5a78529b56358efab67fe06", + "zh:c924c7ac3a8fc08eff7588627be68fc94958c1aaa65928a9fd73cf1d610a0dbf", + "zh:cfdfaeab3fcb522a806fc5b71cbd6096df0fafa06cea2131c0db6074b3b76eed", + "zh:d554393736b99bd1f0b60e210e276531bcd8df79f435924879eeecc1a2100a0d", + "zh:df827b0e00c9e2d666cfe6409f61446908e5983a07ae32c822ef193f6b56c37c", + ] +} diff --git a/deployment/modules/cloudflare/docs-release/config.tf b/deployment/modules/cloudflare/docs-release/config.tf new file mode 100644 index 0000000000..7c3eda4f87 --- /dev/null +++ b/deployment/modules/cloudflare/docs-release/config.tf @@ -0,0 +1,11 @@ +terraform { + backend "pg" {} + required_version = "~> 1.7" + + required_providers { + cloudflare = { + source = "cloudflare/cloudflare" + version = "4.33.0" + } + } +} diff --git a/deployment/modules/cloudflare/docs-release/domain.tf b/deployment/modules/cloudflare/docs-release/domain.tf new file mode 100644 index 0000000000..1221ea595a --- /dev/null +++ b/deployment/modules/cloudflare/docs-release/domain.tf @@ -0,0 +1,8 @@ +resource "cloudflare_record" "immich_app_release_domain" { + name = "immich.app" + proxied = true + ttl = 1 + type = "CNAME" + value = data.terraform_remote_state.cloudflare_immich_app_docs.outputs.immich_app_branch_subdomain + zone_id = data.terraform_remote_state.cloudflare_account.outputs.immich_app_zone_id +} diff --git a/deployment/modules/cloudflare/docs-release/providers.tf b/deployment/modules/cloudflare/docs-release/providers.tf new file mode 100644 index 0000000000..65d0883a9d --- /dev/null +++ b/deployment/modules/cloudflare/docs-release/providers.tf @@ -0,0 +1,3 @@ +provider "cloudflare" { + api_token = data.terraform_remote_state.api_keys_state.outputs.terraform_key_cloudflare_docs +} diff --git a/deployment/modules/cloudflare/docs-release/remote-state.tf b/deployment/modules/cloudflare/docs-release/remote-state.tf new file mode 100644 index 0000000000..3db42dbfff --- /dev/null +++ b/deployment/modules/cloudflare/docs-release/remote-state.tf @@ -0,0 +1,27 @@ +data "terraform_remote_state" "api_keys_state" { + backend = "pg" + + config = { + conn_str = var.tf_state_postgres_conn_str + schema_name = "prod_cloudflare_api_keys" + } +} + +data "terraform_remote_state" "cloudflare_account" { + backend = "pg" + + config = { + conn_str = var.tf_state_postgres_conn_str + schema_name = "prod_cloudflare_account" + } +} + +data "terraform_remote_state" "cloudflare_immich_app_docs" { + backend = "pg" + + config = { + conn_str = var.tf_state_postgres_conn_str + schema_name = "prod_cloudflare_immich_app_docs_${var.prefix_name}" + } +} + diff --git a/deployment/modules/cloudflare/docs-release/terragrunt.hcl b/deployment/modules/cloudflare/docs-release/terragrunt.hcl new file mode 100644 index 0000000000..c3a6f6acae --- /dev/null +++ b/deployment/modules/cloudflare/docs-release/terragrunt.hcl @@ -0,0 +1,20 @@ +terraform { + source = "." + + extra_arguments custom_vars { + commands = get_terraform_commands_that_need_vars() + } +} + +include { + path = find_in_parent_folders("state.hcl") +} + +remote_state { + backend = "pg" + + config = { + conn_str = get_env("TF_STATE_POSTGRES_CONN_STR") + schema_name = "prod_cloudflare_immich_app_docs_release" + } +} diff --git a/deployment/modules/cloudflare/docs-release/variables.tf b/deployment/modules/cloudflare/docs-release/variables.tf new file mode 100644 index 0000000000..6a219bf4ec --- /dev/null +++ b/deployment/modules/cloudflare/docs-release/variables.tf @@ -0,0 +1,4 @@ +variable "cloudflare_account_id" {} +variable "tf_state_postgres_conn_str" {} + +variable "prefix_name" {} diff --git a/deployment/modules/cloudflare/docs/.terraform.lock.hcl b/deployment/modules/cloudflare/docs/.terraform.lock.hcl new file mode 100644 index 0000000000..91dcc1a19d --- /dev/null +++ b/deployment/modules/cloudflare/docs/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "tofu init". +# Manual edits may be lost in future updates. + +provider "registry.opentofu.org/cloudflare/cloudflare" { + version = "4.33.0" + constraints = "4.33.0" + hashes = [ + "h1:jfvnxO1kjxUquizrBlswsytWQeHXjvNz6IZwPTuixJ4=", + "zh:1839d03c8c30d3eac4f18c78c5c095d44348eb06cc9d758136d16517094b64e3", + "zh:1fd94992cbb8ce2943cf4fe3deff01b9b1a0e913d2cdd484ceb96a859d46dc5d", + "zh:2b4f37c16a7f6d3712b03980b52b149d4ef6a544917bfe1d1c2ca2d40468daa5", + "zh:32ecb8017be0a34f72b9b0fcd43d944b99cdc903c79892a68b48719824fb194c", + "zh:6468dee137fa07b0b43f742cfdaabb6620883b00773af370e10755ba579eb7f7", + "zh:766504de95a418fd763d9474f39fb147053201d6c4efa3efa456fb39a559b28b", + "zh:800342078c0c04a36cb2558d0c5c6bf050a8b4c231abecac59e56c9868b9fa7e", + "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", + "zh:94420269672dc5bef37e1a8efab656ada5c2d6f162b52cc59ea74cf0cf35d633", + "zh:972481b86636f00771c2b5f3e408ffd9b66d42942645c8b8e11c4f4bf52285f1", + "zh:ac52d6698fa8b2a8ab820ffb59381da27684a205f5a78529b56358efab67fe06", + "zh:c924c7ac3a8fc08eff7588627be68fc94958c1aaa65928a9fd73cf1d610a0dbf", + "zh:cfdfaeab3fcb522a806fc5b71cbd6096df0fafa06cea2131c0db6074b3b76eed", + "zh:d554393736b99bd1f0b60e210e276531bcd8df79f435924879eeecc1a2100a0d", + "zh:df827b0e00c9e2d666cfe6409f61446908e5983a07ae32c822ef193f6b56c37c", + ] +} diff --git a/deployment/modules/cloudflare/docs/config.tf b/deployment/modules/cloudflare/docs/config.tf new file mode 100644 index 0000000000..7c3eda4f87 --- /dev/null +++ b/deployment/modules/cloudflare/docs/config.tf @@ -0,0 +1,11 @@ +terraform { + backend "pg" {} + required_version = "~> 1.7" + + required_providers { + cloudflare = { + source = "cloudflare/cloudflare" + version = "4.33.0" + } + } +} diff --git a/deployment/modules/cloudflare/docs/domain.tf b/deployment/modules/cloudflare/docs/domain.tf new file mode 100644 index 0000000000..5a6d2122b2 --- /dev/null +++ b/deployment/modules/cloudflare/docs/domain.tf @@ -0,0 +1,18 @@ +resource "cloudflare_pages_domain" "immich_app_branch_domain" { + account_id = var.cloudflare_account_id + project_name = data.terraform_remote_state.cloudflare_account.outputs.immich_app_pages_project_name + domain = "${var.prefix_name}.${local.deploy_domain_prefix}.immich.app" +} + +resource "cloudflare_record" "immich_app_branch_subdomain" { + name = "${var.prefix_name}.${local.deploy_domain_prefix}.immich.app" + proxied = true + ttl = 1 + type = "CNAME" + value = "${replace(var.prefix_name, "/\\/|\\./", "-")}.${data.terraform_remote_state.cloudflare_account.outputs.immich_app_pages_project_subdomain}" + zone_id = data.terraform_remote_state.cloudflare_account.outputs.immich_app_zone_id +} + +output "immich_app_branch_subdomain" { + value = cloudflare_record.immich_app_branch_subdomain.hostname +} diff --git a/deployment/modules/cloudflare/docs/locals.tf b/deployment/modules/cloudflare/docs/locals.tf new file mode 100644 index 0000000000..d830687791 --- /dev/null +++ b/deployment/modules/cloudflare/docs/locals.tf @@ -0,0 +1,7 @@ +locals { + domain_name = "immich.app" + preview_prefix = contains(["branch", "pr"], var.prefix_event_type) ? "preview" : "" + archive_prefix = contains(["release"], var.prefix_event_type) ? "archive" : "" + deploy_domain_prefix = coalesce(local.preview_prefix, local.archive_prefix) + is_release = contains(["release"], var.prefix_event_type) +} diff --git a/deployment/modules/cloudflare/docs/providers.tf b/deployment/modules/cloudflare/docs/providers.tf new file mode 100644 index 0000000000..65d0883a9d --- /dev/null +++ b/deployment/modules/cloudflare/docs/providers.tf @@ -0,0 +1,3 @@ +provider "cloudflare" { + api_token = data.terraform_remote_state.api_keys_state.outputs.terraform_key_cloudflare_docs +} diff --git a/deployment/modules/cloudflare/docs/remote-state.tf b/deployment/modules/cloudflare/docs/remote-state.tf new file mode 100644 index 0000000000..cf9d99f8f5 --- /dev/null +++ b/deployment/modules/cloudflare/docs/remote-state.tf @@ -0,0 +1,17 @@ +data "terraform_remote_state" "api_keys_state" { + backend = "pg" + + config = { + conn_str = var.tf_state_postgres_conn_str + schema_name = "prod_cloudflare_api_keys" + } +} + +data "terraform_remote_state" "cloudflare_account" { + backend = "pg" + + config = { + conn_str = var.tf_state_postgres_conn_str + schema_name = "prod_cloudflare_account" + } +} diff --git a/deployment/modules/cloudflare/docs/terragrunt.hcl b/deployment/modules/cloudflare/docs/terragrunt.hcl new file mode 100644 index 0000000000..95d7b6879b --- /dev/null +++ b/deployment/modules/cloudflare/docs/terragrunt.hcl @@ -0,0 +1,24 @@ +terraform { + source = "." + + extra_arguments custom_vars { + commands = get_terraform_commands_that_need_vars() + } +} + +include { + path = find_in_parent_folders("state.hcl") +} + +locals { + prefix_name = get_env("TF_VAR_prefix_name") +} + +remote_state { + backend = "pg" + + config = { + conn_str = get_env("TF_STATE_POSTGRES_CONN_STR") + schema_name = "prod_cloudflare_immich_app_docs_${local.prefix_name}" + } +} diff --git a/deployment/modules/cloudflare/docs/variables.tf b/deployment/modules/cloudflare/docs/variables.tf new file mode 100644 index 0000000000..9cce2ba770 --- /dev/null +++ b/deployment/modules/cloudflare/docs/variables.tf @@ -0,0 +1,5 @@ +variable "cloudflare_account_id" {} +variable "tf_state_postgres_conn_str" {} + +variable "prefix_name" {} +variable "prefix_event_type" {} diff --git a/deployment/state.hcl b/deployment/state.hcl new file mode 100644 index 0000000000..5c3fc7cfa9 --- /dev/null +++ b/deployment/state.hcl @@ -0,0 +1,20 @@ +locals { + cloudflare_account_id = get_env("CLOUDFLARE_ACCOUNT_ID") + cloudflare_api_token = get_env("CLOUDFLARE_API_TOKEN") + + tf_state_postgres_conn_str = get_env("TF_STATE_POSTGRES_CONN_STR") +} + +remote_state { + backend = "pg" + + config = { + conn_str = local.tf_state_postgres_conn_str + } +} + +inputs = { + cloudflare_account_id = local.cloudflare_account_id + cloudflare_api_token = local.cloudflare_api_token + tf_state_postgres_conn_str = local.tf_state_postgres_conn_str +}