diff --git a/.github/workflows/container-scaleway-qa.yml b/.github/workflows/container-scaleway-qa.yml new file mode 100644 index 0000000..f516bd5 --- /dev/null +++ b/.github/workflows/container-scaleway-qa.yml @@ -0,0 +1,198 @@ +name: "Iconica Standard Scaleway container deployment" + +# Should only be used in workflows executed in PR's + +# https://docs.github.com/en/actions/using-workflows/reusing-workflows +on: + workflow_call: + inputs: + scaleway_project_id: + type: "string" + description: "The ID of the Scaleway project to deploy for" + required: true + scaleway_organization_id: + type: "string" + description: "The ID of the Scaleway organization to deploy for" + required: true + scaleway_region: + type: "string" + description: "The region to deploy the services to" + required: true + + deploy_database: + type: "boolean" + description: "Whether the application needs a PostgreSQL database deployed (default=false)" + required: false + default: false + + container_context: + type: "string" + description: "Subfolder where the container context is located (default=.)" + required: false + default: "." + container_file: + type: "string" + description: "Name of the container file to build (default=Containerfile)" + required: false + default: "Containerfile" + container_build_args: + type: "string" + description: "Additional build arguments to pass along while building the container (default=none)" + required: false + default: "" + container_port: + type: "string" + description: "The port the deployed container should publish (default=8080)" + required: false + default: "8080" + container_deployment_extra_arguments: + type: "string" + description: "Extra arguments to pass when deploying the container. Can be used for setting environment variables for example (default=none)" + required: false + default: "" + container_registry: + type: "string" + description: "Registry to push the container image to" + required: true + container_name: + type: "string" + description: "Name of the final container image" + required: true + container_deployment_namespace_id: + type: "string" + description: "ID of the container namespace that contains the required environment variables and secrets for the container to run" + required: true + secrets: + scaleway_access_key: + description: "The Scaleway access key to authenticate with" + required: true + scaleway_secret_key: + description: "The Scaleway secret key to authenticate with" + required: true + + outputs: + url: + description: "The URL of the deployed container" + value: "${{ jobs.deploy.outputs.url }}" + +jobs: + build: + name: "Build the container" + runs-on: "ubuntu-latest" + steps: + - name: "Clone the code" + uses: "actions/checkout@v7" + - name: "Setup Docker Buildx" + uses: "docker/setup-buildx-action@v4" + - name: "Login to Scaleway container registry" + uses: "docker/login-action@v3" + with: + username: "nologin" + password: "${{ secrets.scaleway_secret_key }}" + registry: "${{ inputs.container_registry }}" + + # Build + - name: "Build and push the container image" + uses: "docker/build-push-action@v7" + with: + context: "${{ inputs.container_context }}" + push: true + file: "${{ inputs.container_file }}" + build-args: ${{ inputs.container_build_args }} + tags: "${{ inputs.container_registry }}/qa-test:qa-${{ github.run_id }}" + cache-from: "type=gha" + cache-to: "type=gha,mode=max" + + deploy: + name: "Deploy the container on Scaleway" + needs: "build" + runs-on: "ubuntu-latest" + outputs: + url: "${{ fromJson(steps.container.outputs.json).public_endpoint }}" + steps: + - name: "Compute tags" + id: "tags" + run: | + echo "QA_ENVIRONMENT=qa-${{ github.run_id }}" >> $GITHUB_OUTPUT + echo "CLEANUP_AFTER=$(date -u -d '+7 days' '+%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT + + # VPC + - name: "[Scaleway] Create VPC" + uses: "scaleway/action-scw@v0.0.3" + id: "vpc" + with: + access-key: "${{ secrets.scaleway_access_key }}" + secret-key: "${{ secrets.scaleway_secret_key }}" + default-project-id: "${{ inputs.scaleway_project_id }}" + default-organization-id: "${{ inputs.scaleway_organization_id }}" + args: > + vpc private-network create region=${{ inputs.scaleway_region }} + name=${{ steps.tags.outputs.QA_ENVIRONMENT }} + tags.0=qa-env=${{ steps.tags.outputs.QA_ENVIRONMENT }} + tags.1=cleanup-after=${{ steps.tags.outputs.CLEANUP_AFTER }} + tags.2=ephemeral=true + tags.3=pr-id=${{ github.event.number }} + + # Database + - name: "[Scaleway] Create database instance" + uses: "scaleway/action-scw@v0.0.3" + id: "database_instance" + if: "${{ inputs.deploy_database }}" + with: + access-key: "${{ secrets.scaleway_access_key }}" + secret-key: "${{ secrets.scaleway_secret_key }}" + default-project-id: "${{ inputs.scaleway_project_id }}" + default-organization-id: "${{ inputs.scaleway_organization_id }}" + args: > + rdb instance create --wait region=${{ inputs.scaleway_region }} + name=${{ steps.tags.outputs.QA_ENVIRONMENT }} + engine=PostgreSQL-17 + user-name=qa + password=QA-dev01 + node-type="DB-DEV-S" + init-endpoints.0.private-network.private-network-id=${{ fromJson(steps.vpc.outputs.json).id }} + init-endpoints.0.private-network.enable-ipam=true + disable-backup=true + tags.0=qa-env=${{ steps.tags.outputs.QA_ENVIRONMENT }} + tags.1=cleanup-after=${{ steps.tags.outputs.CLEANUP_AFTER }} + tags.2=ephemeral=true + tags.3=pr-id=${{ github.event.number }} + - name: "[Scaleway] Create database" + uses: "scaleway/action-scw@v0.0.3" + id: "database" + if: "${{ inputs.deploy_database }}" + with: + access-key: "${{ secrets.scaleway_access_key }}" + secret-key: "${{ secrets.scaleway_secret_key }}" + default-project-id: "${{ inputs.scaleway_project_id }}" + default-organization-id: "${{ inputs.scaleway_organization_id }}" + args: "rdb database list region=${{ inputs.scaleway_region }} instance-id=${{ fromJson(steps.database_instance.outputs.json).id }}" + + # Container + - name: "[Scaleway] Deploy container" + uses: "scaleway/action-scw@v0.0.3" + id: "container" + with: + access-key: "${{ secrets.scaleway_access_key }}" + secret-key: "${{ secrets.scaleway_secret_key }}" + default-project-id: "${{ inputs.scaleway_project_id }}" + default-organization-id: "${{ inputs.scaleway_organization_id }}" + args: > + container container create --wait region=${{ inputs.scaleway_region }} + namespace-id=${{ inputs.container_deployment_namespace_id }} + name=qa-${{ github.run_id }} + min-scale=0 max-scale=1 memory-limit-bytes=0.128G mvcpu-limit=100 + image=${{ inputs.container_registry }}/qa-test:qa-${{ github.run_id }} + port=${{ inputs.container_port }} + private-network-id=${{ fromJson(steps.vpc.outputs.json).id }} + https-connections-only=true + tags.0=qa-env=${{ steps.tags.outputs.QA_ENVIRONMENT }} + tags.1=cleanup-after=${{ steps.tags.outputs.CLEANUP_AFTER }} + tags.2=ephemeral=true + tags.3=pr-id=${{ github.event.number }} + ${{ inputs.deploy_database && format('environment-variables.DATABASE_HOST={0}', fromJson(steps.database_instance.outputs.json).endpoints[0].ip) || '' }} + ${{ inputs.deploy_database && format('environment-variables.DATABASE_PORT={0}', fromJson(steps.database_instance.outputs.json).endpoints[0].port) || '' }} + ${{ inputs.deploy_database && format('environment-variables.DATABASE_NAME={0}', fromJson(steps.database_instance.outputs.json).endpoints[0].name) || '' }} + ${{ inputs.deploy_database && 'environment-variables.DATABASE_USER=qa' || '' }} + ${{ inputs.deploy_database && format('secret-environment-variables.DATABASE_PASSWORD={0}', fromJson(steps.database_instance.outputs.json).endpoints[0].password) || '' }} + ${{ inputs.container_deployment_extra_arguments }} diff --git a/.github/workflows/flutter-app-android-release.yaml b/.github/workflows/flutter-app-android-release.yaml index 1c284aa..5237d8f 100644 --- a/.github/workflows/flutter-app-android-release.yaml +++ b/.github/workflows/flutter-app-android-release.yaml @@ -1,4 +1,4 @@ -name: Iconica Standard Flutter app Android release building +name: "Iconica Standard Flutter app Android release building" # Workflow version: 1.0.0 # https://docs.github.com/en/actions/using-workflows/reusing-workflows @@ -55,7 +55,11 @@ on: jobs: build: + name: "Build the app for Android" runs-on: "${{ inputs.runs_on }}" + timeout-minutes: 20 + outputs: + artifact_url: "${{ steps.artifact.outputs.artifact-url }}" steps: - name: "Clone the code" uses: "actions/checkout@v7" @@ -103,6 +107,7 @@ jobs: # Upload - name: "Upload Android release" uses: "actions/upload-artifact@v7" + id: "artifact" with: name: "${{ inputs.artifact_output_name }}" path: "${{ inputs.subfolder }}/build/app/outputs/apk/release/app-release.apk" diff --git a/.github/workflows/flutter-app-web-qa.yml b/.github/workflows/flutter-app-web-qa.yml new file mode 100644 index 0000000..0960086 --- /dev/null +++ b/.github/workflows/flutter-app-web-qa.yml @@ -0,0 +1,127 @@ +name: "Build and deploy a release version of the Flutter web app" + +# https://docs.github.com/en/actions/using-workflows/reusing-workflows +on: + workflow_call: + inputs: + scaleway_project_id: + type: "string" + description: "The ID of the Scaleway project to deploy for" + required: true + scaleway_organization_id: + type: "string" + description: "The ID of the Scaleway organization to deploy for" + required: true + scaleway_region: + type: "string" + description: "The region to deploy the services to" + required: true + + container_registry: + type: "string" + description: "Registry to push the container image to" + required: true + container_deployment_namespace_id: + type: "string" + description: "ID of the container namespace that contains the required environment variables and secrets for the container to run" + required: true + + container_context: + type: "string" + description: "Subfolder where the container context is located (default=.)" + required: false + default: "." + container_file: + type: "string" + description: "Name of the container file to build (default=Containerfile)" + required: false + default: "Containerfile" + container_build_args: + type: "string" + description: "Additional build arguments to pass along while building the container (default=none)" + required: false + default: "" + container_port: + type: "string" + description: "The port the deployed container should publish (default=80)" + required: false + default: "80" + pre_build_commands: + type: "string" + description: "Any commands to be executed before building" + required: false + default: "" + + secrets: + scaleway_access_key: + description: "The Scaleway access key to authenticate with" + required: true + scaleway_secret_key: + description: "The Scaleway secret key to authenticate with" + required: true + +jobs: + build: + name: "Build the app" + runs-on: "ubuntu-latest" + steps: + - name: "Clone the code" + uses: "actions/checkout@v7" + - name: "Setup Docker Buildx" + uses: "docker/setup-buildx-action@v4" + - name: "Login to Scaleway container registry" + uses: "docker/login-action@v3" + with: + username: "nologin" + password: "${{ secrets.scaleway_secret_key }}" + registry: "${{ inputs.container_registry }}" + + - name: "Execute commands before building" + if: "${{ inputs.pre_build_commands != '' }}" + run: "${{ inputs.pre_build_commands }}" + + # Build + - name: "Build and push the container image" + uses: "docker/build-push-action@v7" + with: + context: "${{ inputs.container_context }}" + push: true + file: "${{ inputs.container_file }}" + build-args: ${{ inputs.container_build_args }} + tags: "${{ inputs.container_registry }}/qa-flutter-web:qa-${{ github.run_id }}" + cache-from: "type=gha" + cache-to: "type=gha,mode=max" + + deploy: + name: "[Scaleway] Deploy the app" + needs: "build" + runs-on: "ubuntu-latest" + outputs: + url: "${{ fromJson(steps.container.outputs.json).public_endpoint }}" + steps: + - name: "Compute tags" + id: "tags" + run: | + echo "QA_ENVIRONMENT=qa-${{ github.run_id }}" >> $GITHUB_OUTPUT + echo "CLEANUP_AFTER=$(date -u -d '+7 days' '+%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT + + - name: "Deploy container on Scaleway" + uses: "scaleway/action-scw@v0.0.3" + id: "container" + with: + access-key: "${{ secrets.scaleway_access_key }}" + secret-key: "${{ secrets.scaleway_secret_key }}" + default-project-id: "${{ inputs.scaleway_project_id }}" + default-organization-id: "${{ inputs.scaleway_organization_id }}" + args: | + container container create --wait region=${{ inputs.scaleway_region }} + namespace-id=${{ inputs.container_deployment_namespace_id }} + name=qa-web-${{ github.run_id }} + min-scale=0 max-scale=1 memory-limit-bytes=0.128G mvcpu-limit=100 + image=${{ inputs.container_registry }}/qa-test:qa-${{ github.run_id }} + port=${{ inputs.container_port }} + https-connections-only=true + tags.0=qa-env=${{ steps.tags.outputs.QA_ENVIRONMENT }} + tags.1=cleanup-after=${{ steps.tags.outputs.CLEANUP_AFTER }} + tags.2=ephemeral=true + tags.3=pr-id=${{ github.event.number }} diff --git a/.github/workflows/scaleway-cleanup.yml b/.github/workflows/scaleway-cleanup.yml new file mode 100644 index 0000000..ea81d49 --- /dev/null +++ b/.github/workflows/scaleway-cleanup.yml @@ -0,0 +1,140 @@ +name: "Cleanup any Scaleway deployments" + +on: + workflow_call: + inputs: + scaleway_project_id: + type: "string" + description: "The ID of the Scaleway project to deploy for" + required: true + scaleway_organization_id: + type: "string" + description: "The ID of the Scaleway organization to deploy for" + required: true + scaleway_region: + type: "string" + description: "The region to deploy the services to" + required: true + + reap_filter: + type: "string" + description: "The tag to filter resources to cleanup for" + required: true + secrets: + scaleway_access_key: + description: "The Scaleway access key to authenticate with" + required: true + scaleway_secret_key: + description: "The Scaleway secret key to authenticate with" + required: true + +jobs: + determine-reap: + name: "Determine resources to reap" + runs-on: "ubuntu-latest" + outputs: + container_ids: "${{ steps.container_ids.outputs.ids }}" + database_instance_ids: "${{ steps.database_instance_ids.outputs.ids }}" + vpc_ids: "${{ steps.vpc_ids.outputs.ids }}" + steps: + # Containers + - name: "[Scaleway] Get containers" + uses: "scaleway/action-scw@v0.0.3" + id: "containers" + with: + access-key: "${{ secrets.scaleway_access_key }}" + secret-key: "${{ secrets.scaleway_secret_key }}" + default-project-id: "${{ inputs.scaleway_project_id }}" + default-organization-id: "${{ inputs.scaleway_organization_id }}" + args: "container container list region=${{ inputs.scaleway_region }}" + - name: "[Scaleway] Determine containers to reap" + id: "container_ids" + run: | + IDS=$(echo '${{ steps.containers.outputs.json }}' | jq -c '[.[] | select(.tags[] == "${{ inputs.reap_filter }}") | .id']) + echo "ids=$IDS" >> $GITHUB_OUTPUT + + # Databases + - name: "[Scaleway] Get databases" + uses: "scaleway/action-scw@v0.0.3" + id: "databases" + with: + access-key: "${{ secrets.scaleway_access_key }}" + secret-key: "${{ secrets.scaleway_secret_key }}" + default-project-id: "${{ inputs.scaleway_project_id }}" + default-organization-id: "${{ inputs.scaleway_organization_id }}" + args: "rdb instance list region=${{ inputs.scaleway_region }}" + - name: "[Scaleway] Determine databases to reap" + id: "database_instance_ids" + run: | + IDS=$(echo '${{ steps.databases.outputs.json }}' | jq -c '[.[] | select(.tags[] == "${{ inputs.reap_filter }}") | .id']) + echo "ids=$IDS" >> $GITHUB_OUTPUT + + # Networks + - name: "[Scaleway] Get VPC's" + uses: "scaleway/action-scw@v0.0.3" + id: "vpc" + with: + access-key: "${{ secrets.scaleway_access_key }}" + secret-key: "${{ secrets.scaleway_secret_key }}" + default-project-id: "${{ inputs.scaleway_project_id }}" + default-organization-id: "${{ inputs.scaleway_organization_id }}" + args: "vpc private-network list region=${{ inputs.scaleway_region }}" + - name: "[Scaleway] Determine networks to reap" + id: "vpc_ids" + run: | + IDS=$(echo '${{ steps.vpc.outputs.json }}' | jq -c '[.[] | select(.tags[] == "${{ inputs.reap_filter }}") | .id']) + echo "ids=$IDS" >> $GITHUB_OUTPUT + + reap-containers: + name: "Reap containers" + needs: "determine-reap" + if: "${{ needs.determine-reap.outputs.container_ids != '' }}" + runs-on: "ubuntu-latest" + strategy: + matrix: + container_id: ${{ fromJson(needs.determine-reap.outputs.container_ids) }} + steps: + - name: "[Scaleway] Reap containers" + uses: "scaleway/action-scw@v0.0.3" + with: + access-key: "${{ secrets.scaleway_access_key }}" + secret-key: "${{ secrets.scaleway_secret_key }}" + default-project-id: "${{ inputs.scaleway_project_id }}" + default-organization-id: "${{ inputs.scaleway_organization_id }}" + args: "container container delete region=${{ inputs.scaleway_region }} ${{ matrix.container_id }}" + + reap-databases: + name: "Reap databases" + needs: ["determine-reap", "reap-containers"] + if: "${{ always() && needs.determine-reap.outputs.database_instance_ids != '' }}" + runs-on: "ubuntu-latest" + strategy: + matrix: + database_instance_id: ${{ fromJson(needs.determine-reap.outputs.database_instance_ids) }} + steps: + - name: "[Scaleway] Reap databases" + uses: "scaleway/action-scw@v0.0.3" + with: + access-key: "${{ secrets.scaleway_access_key }}" + secret-key: "${{ secrets.scaleway_secret_key }}" + default-project-id: "${{ inputs.scaleway_project_id }}" + default-organization-id: "${{ inputs.scaleway_organization_id }}" + args: "rdb instance delete region=${{ inputs.scaleway_region }} ${{ matrix.database_instance_id }}" + + reap-vpcs: + name: "Reap VPC's" + needs: ["determine-reap", "reap-containers", "reap-databases"] + if: "${{ always() && needs.determine-reap.outputs.vpc_ids != '' }}" + runs-on: "ubuntu-latest" + strategy: + matrix: + vpc_id: ${{ fromJson(needs.determine-reap.outputs.vpc_ids) }} + steps: + - name: "[Scaleway] Reap VPC's" + uses: "scaleway/action-scw@v0.0.3" + with: + access-key: "${{ secrets.scaleway_access_key }}" + secret-key: "${{ secrets.scaleway_secret_key }}" + default-project-id: "${{ inputs.scaleway_project_id }}" + default-organization-id: "${{ inputs.scaleway_organization_id }}" + args: "vpc private-network delete region=${{ inputs.scaleway_region }} ${{ matrix.vpc_id }}"