diff --git a/.github/actions/deploy-web-app/action.yml b/.github/actions/deploy-web-app/action.yml deleted file mode 100644 index 3ea8a343ed2..00000000000 --- a/.github/actions/deploy-web-app/action.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: "Deploy web app" -description: "Build a web app and publish it to Cloudflare Pages" - -inputs: - app: - description: "Web app slug" - required: true - output: - description: "Build output directory within web/apps/" - default: "out" - account-id: - description: "Cloudflare account ID" - required: true - api-token: - description: "Cloudflare API token" - required: true - -runs: - using: "composite" - steps: - - name: Build ${{ inputs.app }} - shell: bash - working-directory: web - run: npm run build:${{ inputs.app }} - - - name: Publish ${{ inputs.app }} - uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0 - with: - accountId: ${{ inputs.account-id }} - apiToken: ${{ inputs.api-token }} - command: pages deploy --project-name=ente --commit-dirty=true --branch=deploy/${{ inputs.app }} web/apps/${{ inputs.app }}/${{ inputs.output }} diff --git a/.github/scripts/check_workflow_security.rb b/.github/scripts/check_workflow_security.rb index 91863f694bc..a92f8c4ea8a 100644 --- a/.github/scripts/check_workflow_security.rb +++ b/.github/scripts/check_workflow_security.rb @@ -3,104 +3,314 @@ require "set" require "yaml" -SENSITIVE_TRIGGERS = %w[ - pull_request_target - issue_comment - pull_request_review_comment - discussion_comment - workflow_run -].to_set.freeze - -CHECKED_PATHS = [ - ".github/workflows/*.{yml,yaml}", - ".github/actions/**/*.{yml,yaml}", -].freeze -USES_REF = %r{\A([A-Za-z0-9._-]+/[A-Za-z0-9._-]+(?:/[A-Za-z0-9._/-]+)?)@(\S+)\z} -FULL_SHA = /\A[0-9a-fA-F]{40}\z/ - -def workflow_yaml(path) - YAML.safe_load( - File.read(path), - aliases: true, - ) || {} -rescue Psych::Exception => e - abort("Failed to parse workflow YAML in #{path}: #{e.message}") -end +class WorkflowSecurityChecker + FORK_GUARD = "github.repository == 'vanton1/ente'".freeze + ALLOWED_RUNNERS = %w[ubuntu-24.04 macos-26].to_set.freeze + SENSITIVE_TRIGGERS = %w[ + pull_request_target + issue_comment + pull_request_review_comment + discussion_comment + workflow_run + ].to_set.freeze + WORKFLOW_RULES = { + ".github/workflows/codeql.yml" => { + name: "CodeQL (GitHub Actions)", + triggers: %w[pull_request schedule workflow_dispatch], + permissions: { "contents" => "read", "pull-requests" => "read", "security-events" => "write" }, + jobs: %w[analyze-actions], + check_names: { "analyze-actions" => "Actions CodeQL gate" }, + }, + ".github/workflows/dependency-review.yml" => { + name: "Dependency review", + triggers: %w[pull_request], + permissions: { "contents" => "read" }, + jobs: %w[dependency-review], + check_names: { "dependency-review" => "Dependency review gate" }, + }, + ".github/workflows/self-hosted-mobile-linux.yml" => { + name: "Self-hosted mobile (Linux)", + triggers: %w[pull_request workflow_dispatch], + permissions: { "contents" => "read", "pull-requests" => "read" }, + jobs: %w[validate], + check_names: { "validate" => "Linux mobile gate" }, + }, + ".github/workflows/self-hosted-mobile-macos.yml" => { + name: "Self-hosted mobile (macOS)", + triggers: %w[pull_request workflow_dispatch], + permissions: { "contents" => "read", "pull-requests" => "read" }, + jobs: %w[changes gate validate], + check_names: { + "changes" => "Detect macOS mobile changes", + "gate" => "macOS mobile gate", + "validate" => "macOS mobile validation", + }, + }, + ".github/workflows/upstream-sync-drift.yml" => { + name: "Upstream sync drift", + triggers: %w[schedule workflow_dispatch], + permissions: { "contents" => "read", "issues" => "write" }, + jobs: %w[detect], + check_names: {}, + }, + ".github/workflows/workflow-security-checks.yml" => { + name: "Workflow security checks", + triggers: %w[pull_request], + permissions: { "contents" => "read", "pull-requests" => "read" }, + jobs: %w[changes gate validate], + check_names: { + "changes" => "Detect workflow changes", + "gate" => "Workflow security gate", + "validate" => "Workflow security validation", + }, + }, + }.freeze + ALLOWED_ACTION_FILES = Set[ + ".github/actions/setup-flutter/action.yml", + ].freeze + ALLOWED_ENVIRONMENTS = { + ".github/workflows/workflow-security-checks.yml" => { + "validate" => "workflow-change-approval", + }, + }.freeze + USES_REF = %r{\A([A-Za-z0-9._-]+/[A-Za-z0-9._-]+(?:/[A-Za-z0-9._/-]+)?)@(\S+)\z}.freeze + FULL_SHA = /\A[0-9a-fA-F]{40}\z/.freeze + + def initialize(root: Dir.pwd, out: $stdout) + @root = File.expand_path(root) + @out = out + @violations = Hash.new { |hash, key| hash[key] = [] } + end -def trigger_names(workflow) - events = workflow["on"] || workflow[true] - return [events] if events.is_a?(String) - return events.grep(String) if events.is_a?(Array) - return events.keys.map(&:to_s) if events.is_a?(Hash) + def run + workflow_paths = relative_glob(".github/workflows/*.{yml,yaml}") + action_paths = relative_glob(".github/actions/**/*.{yml,yaml}") - [] -end + validate_allowlist("workflow", workflow_paths.to_set, WORKFLOW_RULES.keys.to_set) + validate_allowlist("action", action_paths.to_set, ALLOWED_ACTION_FILES) + + (workflow_paths + action_paths).each do |path| + document = workflow_yaml(path) + validate_uses(path, document) + validate_checkout_credentials(path, document) + validate_secret_references(path) + end + + workflow_paths.each do |path| + rule = WORKFLOW_RULES[path] + next unless rule + + workflow = workflow_yaml(path) + validate_name(path, workflow, rule.fetch(:name)) + validate_triggers(path, workflow, rule.fetch(:triggers)) + validate_pull_request_gate(path, workflow) + validate_permissions(path, workflow, rule.fetch(:permissions)) + validate_jobs(path, workflow, rule.fetch(:jobs), rule.fetch(:check_names)) + end + + print_report(workflow_paths.length + action_paths.length) + @violations.empty? ? 0 : 1 + end + + private + + def relative_glob(pattern) + Dir.glob(File.join(@root, pattern)).sort.map do |path| + path.delete_prefix("#{@root}/") + end + end + + def workflow_yaml(path) + YAML.safe_load(File.read(File.join(@root, path)), aliases: true) || {} + rescue Psych::Exception => e + add(:yaml, "#{path}: #{e.message}") + {} + end + + def trigger_names(workflow) + events = workflow["on"] || workflow[true] + return [events] if events.is_a?(String) + return events.grep(String) if events.is_a?(Array) + return events.keys.map(&:to_s) if events.is_a?(Hash) -def uses_values(node) - case node - when Hash - node.flat_map do |key, value| - [key.to_s == "uses" && value.is_a?(String) ? value : nil, *uses_values(value)] - end.compact - when Array - node.flat_map { |value| uses_values(value) } - else [] end -end -def workflow_facts(path) - workflow = workflow_yaml(path) - triggers = trigger_names(workflow).to_set & SENSITIVE_TRIGGERS - unpinned_actions = uses_values(workflow).each_with_object(Set.new) do |uses, actions| - action, ref = uses.match(USES_REF)&.captures - next unless action - next if ref.match?(FULL_SHA) + def validate_allowlist(kind, actual, expected) + (actual - expected).sort.each do |path| + add(:allowlist, "Unexpected #{kind}: #{path}") + end + (expected - actual).sort.each do |path| + add(:allowlist, "Missing #{kind}: #{path}") + end + end - actions.add("#{action}@#{ref}") + def validate_triggers(path, workflow, expected) + actual = trigger_names(workflow).to_set + sensitive = actual & SENSITIVE_TRIGGERS + sensitive.each { |trigger| add(:triggers, "#{path}: privileged trigger #{trigger}") } + return if actual == expected.to_set + + add(:triggers, "#{path}: expected #{expected.sort.join(', ')}, found #{actual.to_a.sort.join(', ')}") end - { triggers: triggers, unpinned_actions: unpinned_actions } -end + def validate_name(path, workflow, expected) + actual = workflow["name"] + add(:identity, "#{path}: expected name #{expected.inspect}, found #{actual.inspect}") unless actual == expected + end -abort("Usage: #{$PROGRAM_NAME}") unless ARGV.empty? + def validate_pull_request_gate(path, workflow) + events = workflow["on"] || workflow[true] + return unless events.is_a?(Hash) && events.key?("pull_request") -checked_files = CHECKED_PATHS.flat_map { |path| Dir.glob(path) }.sort + configuration = events["pull_request"] + return if configuration.nil? || configuration == {} -trigger_violations = [] -unpinned_violations = [] + add(:triggers, "#{path}: pull_request must always create a stable check; filter paths inside jobs") + end + + def validate_permissions(path, workflow, expected) + actual = stringify_hash(workflow["permissions"] || {}) + return if actual == expected -checked_files.each do |path| - facts = workflow_facts(path) - facts[:triggers].each do |trigger| - trigger_violations << "#{path}: #{trigger}" + add(:permissions, "#{path}: expected #{expected.inspect}, found #{actual.inspect}") end - facts[:unpinned_actions].each do |action| - unpinned_violations << "#{path}: #{action}" + def validate_jobs(path, workflow, expected_jobs, check_names) + jobs = workflow["jobs"] + unless jobs.is_a?(Hash) && !jobs.empty? + add(:jobs, "#{path}: no jobs declared") + return + end + + actual_jobs = jobs.keys.map(&:to_s).sort + unless actual_jobs == expected_jobs.sort + add(:jobs, "#{path}: expected jobs #{expected_jobs.sort.join(', ')}, found #{actual_jobs.join(', ')}") + end + + jobs.each do |name, job| + unless job.is_a?(Hash) + add(:jobs, "#{path}: job #{name} is not a mapping") + next + end + + condition = job["if"].to_s + unless condition.include?(FORK_GUARD) && !condition.include?("||") + add(:jobs, "#{path}: job #{name} must fail closed on #{FORK_GUARD}") + end + + runner = job["runs-on"] + add(:jobs, "#{path}: job #{name} uses unapproved runner #{runner.inspect}") unless ALLOWED_RUNNERS.include?(runner) + + timeout = job["timeout-minutes"] + unless timeout.is_a?(Integer) && timeout.positive? && timeout <= 60 + add(:jobs, "#{path}: job #{name} needs a timeout from 1 to 60 minutes") + end + + expected_check_name = check_names[name.to_s] + if expected_check_name && job["name"] != expected_check_name + add(:identity, "#{path}: job #{name} expected check name #{expected_check_name.inspect}, found #{job['name'].inspect}") + end + + add(:permissions, "#{path}: job #{name} must not override top-level permissions") if job.key?("permissions") + validate_environment(path, name, job) + end end -end -failed = trigger_violations.any? || unpinned_violations.any? -puts "Workflow Security Checks: #{failed ? "Failed" : "Passed"}" -puts "Checked #{checked_files.length} workflow/action files." + def validate_environment(path, job_name, job) + expected = ALLOWED_ENVIRONMENTS.fetch(path, {})[job_name] + environment = job["environment"] + actual = environment.is_a?(Hash) ? environment["name"] : environment + return if actual == expected -exit 0 unless failed + add(:environments, "#{path}: job #{job_name} expected environment #{expected.inspect}, found #{actual.inspect}") + end -unless trigger_violations.empty? - puts - puts "Privileged triggers:" - trigger_violations.each { |violation| puts "- #{violation}" } -end + def validate_uses(path, document) + uses_values(document).each do |uses| + if uses.start_with?("./") + local_action = "#{uses.delete_prefix('./')}/action.yml" + add(:actions, "#{path}: unapproved local action #{uses}") unless ALLOWED_ACTION_FILES.include?(local_action) + next + end + + action, ref = uses.match(USES_REF)&.captures + unless action + add(:actions, "#{path}: unsupported action reference #{uses}") + next + end + add(:actions, "#{path}: unpinned action #{action}@#{ref}") unless ref.match?(FULL_SHA) + end + end + + def validate_checkout_credentials(path, document) + step_nodes(document).each do |step| + uses = step["uses"] + next unless uses.is_a?(String) && uses.start_with?("actions/checkout@") + + value = step.fetch("with", {})["persist-credentials"] + add(:credentials, "#{path}: actions/checkout must set persist-credentials: false") unless value == false + end + end + + def validate_secret_references(path) + source = File.read(File.join(@root, path)) + add(:secrets, "#{path}: repository or environment secret reference is forbidden") if source.match?(/\bsecrets\s*\./) + end -unless unpinned_violations.empty? - puts - puts "Unpinned external actions:" - unpinned_violations.each { |violation| puts "- #{violation}" } + def uses_values(node) + case node + when Hash + node.flat_map do |key, value| + current = key.to_s == "uses" && value.is_a?(String) ? [value] : [] + current + uses_values(value) + end + when Array + node.flat_map { |value| uses_values(value) } + else + [] + end + end + + def step_nodes(node) + case node + when Hash + current = node.key?("uses") ? [node] : [] + current + node.values.flat_map { |value| step_nodes(value) } + when Array + node.flat_map { |value| step_nodes(value) } + else + [] + end + end + + def stringify_hash(value) + return {} unless value.is_a?(Hash) + + value.each_with_object({}) do |(key, item), output| + output[key.to_s] = item.to_s + end + end + + def add(category, message) + @violations[category] << message + end + + def print_report(checked_count) + failed = !@violations.empty? + @out.puts "Workflow Security Checks: #{failed ? 'Failed' : 'Passed'}" + @out.puts "Checked #{checked_count} approved workflow/action files." + return unless failed + + @violations.keys.sort.each do |category| + @out.puts + @out.puts "#{category.to_s.capitalize} violations:" + @violations[category].sort.each { |violation| @out.puts "- #{violation}" } + end + end end -puts -puts "Fix:" -puts "- Remove privileged triggers." if trigger_violations.any? -puts "- Pin external actions to a full 40-character commit SHA." if unpinned_violations.any? -exit 1 +if $PROGRAM_NAME == __FILE__ + abort("Usage: #{$PROGRAM_NAME}") unless ARGV.empty? + exit WorkflowSecurityChecker.new.run +end diff --git a/.github/scripts/check_workflow_security_test.rb b/.github/scripts/check_workflow_security_test.rb new file mode 100644 index 00000000000..94a69c4fb9c --- /dev/null +++ b/.github/scripts/check_workflow_security_test.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +require "fileutils" +require "minitest/autorun" +require "stringio" +require "tmpdir" +require_relative "check_workflow_security" + +class WorkflowSecurityCheckerTest < Minitest::Test + ROOT = File.expand_path("../..", __dir__) + + def test_repository_matches_the_complete_security_contract + status, output = run_checker(ROOT) + + assert_equal 0, status, output + assert_includes output, "Checked 7 approved workflow/action files." + end + + def test_unexpected_and_missing_automation_fail_the_allowlist + with_fixture do |root| + File.write( + File.join(root, ".github/workflows/unapproved.yml"), + "name: Unapproved\non: workflow_dispatch\npermissions: {}\njobs: {}\n", + ) + FileUtils.rm(File.join(root, ".github/workflows/dependency-review.yml")) + + status, output = run_checker(root) + assert_equal 1, status + assert_includes output, "Unexpected workflow: .github/workflows/unapproved.yml" + assert_includes output, "Missing workflow: .github/workflows/dependency-review.yml" + end + end + + def test_unpinned_actions_secrets_and_checkout_credentials_fail_closed + with_fixture do |root| + path = File.join(root, ".github/workflows/codeql.yml") + source = File.read(path) + .sub(/actions\/checkout@[0-9a-f]{40}/, "actions/checkout@main") + .sub("persist-credentials: false", "persist-credentials: true") + .sub("category: /language:actions", "category: ${{ secrets.CODEQL_CATEGORY }}") + File.write(path, source) + + status, output = run_checker(root) + assert_equal 1, status + assert_includes output, "unpinned action actions/checkout@main" + assert_includes output, "actions/checkout must set persist-credentials: false" + assert_includes output, "secret reference is forbidden" + end + end + + def test_trigger_permissions_runner_timeout_guard_and_environment_are_exact + with_fixture do |root| + path = File.join(root, ".github/workflows/dependency-review.yml") + source = File.read(path) + .sub("pull_request:", "pull_request_target:") + .sub("contents: read", "contents: write") + .sub("if: github.repository == 'vanton1/ente'", "if: always()") + .sub("runs-on: ubuntu-24.04", "runs-on: ubuntu-latest") + .sub("timeout-minutes: 10", "timeout-minutes: 0\n environment: production") + File.write(path, source) + + status, output = run_checker(root) + assert_equal 1, status + assert_includes output, "privileged trigger pull_request_target" + assert_includes output, "expected {\"contents\"=>\"read\"}, found {\"contents\"=>\"write\"}" + assert_includes output, "must fail closed" + assert_includes output, "unapproved runner \"ubuntu-latest\"" + assert_includes output, "needs a timeout from 1 to 60 minutes" + assert_includes output, "expected environment nil, found \"production\"" + end + end + + def test_workflow_identity_jobs_and_stable_pull_request_checks_are_exact + with_fixture do |root| + path = File.join(root, ".github/workflows/dependency-review.yml") + source = File.read(path) + .sub("name: Dependency review", "name: Renamed check") + .sub("name: Dependency review gate", "name: Duplicate gate") + .sub("pull_request:\n", "pull_request:\n paths:\n - mobile/**\n") + .sub("jobs:\n", "jobs:\n unexpected: {}\n") + File.write(path, source) + + status, output = run_checker(root) + assert_equal 1, status + assert_includes output, "expected name \"Dependency review\", found \"Renamed check\"" + assert_includes output, "expected check name \"Dependency review gate\", found \"Duplicate gate\"" + assert_includes output, "pull_request must always create a stable check" + assert_includes output, "expected jobs dependency-review, found dependency-review, unexpected" + end + end + + private + + def run_checker(root) + output = StringIO.new + status = WorkflowSecurityChecker.new(root: root, out: output).run + [status, output.string] + end + + def with_fixture + Dir.mktmpdir("workflow-security-") do |root| + FileUtils.mkdir_p(File.join(root, ".github")) + FileUtils.cp_r(File.join(ROOT, ".github/workflows"), File.join(root, ".github")) + FileUtils.cp_r(File.join(ROOT, ".github/actions"), File.join(root, ".github")) + yield root + end + end +end diff --git a/.github/workflows/app-release.yml b/.github/workflows/app-release.yml deleted file mode 100644 index 18762b6297e..00000000000 --- a/.github/workflows/app-release.yml +++ /dev/null @@ -1,288 +0,0 @@ -name: "Release (App)" - -# See .github/docs/app-release.md - -on: - workflow_dispatch: - inputs: - app: - description: "App to release" - required: true - type: choice - options: - - photos - - auth - - locker - - ensu - - photos-desktop - action: - description: "Release action" - required: true - type: choice - options: - - start - - promote - version: - description: "Release version, e.g. 0.1.16" - required: true - type: string - -permissions: - actions: read - contents: write - pull-requests: write - -concurrency: - group: ${{ inputs.app }}-release - cancel-in-progress: false - -jobs: - release: - runs-on: ubuntu-latest - environment: - name: release-branch-push - env: - APP: ${{ inputs.app }} - VERSION: ${{ inputs.version }} - - steps: - - name: Validate version input - shell: bash - run: | - if [[ ! "${VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "::error::version must be x.y.z" - exit 1 - fi - - - name: Checkout main - if: inputs.action == 'start' - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: main - - - name: Checkout release branch - if: inputs.action == 'promote' - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: release/${{ inputs.app }}-v${{ inputs.version }} - - - name: Mint App token - id: app_token - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 - with: - client-id: ${{ secrets.ENTE_CI_GH_CLIENT_ID }} - private-key: ${{ secrets.ENTE_CI_GH_APP_PRIVATE_KEY }} - repositories: ente,nightly,photos-desktop - permission-contents: write - permission-pull-requests: write - - - name: Configure git identity - env: - GH_TOKEN: ${{ steps.app_token.outputs.token }} - APP_SLUG: ${{ steps.app_token.outputs.app-slug }} - run: | - bot_id="$(gh api "/users/${APP_SLUG}[bot]" --jq .id)" - git config --global user.name "${APP_SLUG}[bot]" - git config --global user.email "${bot_id}+${APP_SLUG}[bot]@users.noreply.github.com" - - - name: Start release - if: inputs.action == 'start' - env: - GH_TOKEN: ${{ steps.app_token.outputs.token }} - RUNS_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - - app_version() { - node .github/scripts/app-version.mjs "${APP}" "$@" - } - - git_push_release_branch() { - git -c http.https://github.com/.extraheader= push \ - "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "$@" - } - - release_branch="release/${APP}-v${VERSION}" - beta_tag="${APP}-v${VERSION}-beta" - IFS=. read -r major minor patch <<< "${VERSION}" - expected_main_version="${VERSION}" - next_version="${major}.${minor}.$((patch + 1))" - if [ "${APP}" = "ensu" ] || [ "${APP}" = "photos-desktop" ]; then - expected_main_version="${VERSION}-beta" - next_version="${next_version}-beta" - fi - next_branch="${APP}-v${next_version}" - - main_version="$(app_version get)" - if [ "${main_version}" != "${expected_main_version}" ]; then - echo "::error::main is ${main_version}, expected ${expected_main_version}" - exit 1 - fi - - git switch -c "${release_branch}" - if [ "${APP}" = "photos-desktop" ]; then - app_version set "${VERSION}" - git commit -am "${APP^} v${VERSION}" - else - build_base="$(app_version get-build-base)" - last_run="$(GH_TOKEN="${RUNS_TOKEN}" gh api "/repos/${GITHUB_REPOSITORY}/actions/workflows/${APP}-build.yml/runs?per_page=1" --jq '.workflow_runs[0].run_number // 0')" - app_version set-build-and-commit "${VERSION}" "$((build_base + last_run + 1))" - fi - git_push_release_branch "${release_branch}" - - gh release delete "${beta_tag}" --yes --cleanup-tag --repo ente/nightly || true - git push origin ":refs/tags/${beta_tag}" || true - - git switch -c "${next_branch}" main - app_version set "${next_version}" - CHANGES_DIR="mobile/apps/${APP}/changes" - if [ "${APP}" = "ensu" ]; then - CHANGES_DIR="rust/apps/ensu/changes" - elif [ "${APP}" = "photos-desktop" ]; then - CHANGES_DIR="desktop/changes" - fi - find "${CHANGES_DIR}" -maxdepth 1 -type f -name '*.md' ! -name README.md -delete - git commit -am "Start ${APP^} ${next_version}" - git push -u origin "${next_branch}" - - gh pr create --base main --head "${next_branch}" --title "[${APP}] Start ${APP^} ${next_version}" --body "" - - - name: Promote release - if: inputs.action == 'promote' - env: - GH_TOKEN: ${{ steps.app_token.outputs.token }} - run: | - set -euo pipefail - - app_version() { - node .github/scripts/app-version.mjs "${APP}" "$@" - } - - git_push_release_branch() { - git -c http.https://github.com/.extraheader= push \ - "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "$@" - } - - release_branch="release/${APP}-v${VERSION}" - - if [ "${APP}" = "photos-desktop" ]; then - if ! draft="$(gh release view "v${VERSION}" --repo ente/photos-desktop --json isDraft --jq .isDraft 2>/dev/null)" || [ "${draft}" != "true" ]; then - echo "::error::v${VERSION} draft release does not exist in photos-desktop" - exit 1 - fi - branch_version="$(app_version get)" - if [ "${branch_version}" != "${VERSION}" ]; then - echo "::error::${release_branch} is ${branch_version}, expected ${VERSION}" - exit 1 - fi - release_sha="$(git rev-parse HEAD)" - rc_sha="$(git ls-remote origin "refs/tags/photos-desktop-v${VERSION}-rc" | cut -f1)" - if [ "${rc_sha}" != "${release_sha}" ]; then - echo "::error::photos-desktop-v${VERSION}-rc does not point at ${release_branch}" - exit 1 - fi - - git tag "photos-desktop-v${VERSION}" "${release_sha}" - git push origin "refs/tags/photos-desktop-v${VERSION}" - - git clone --depth 1 "https://x-access-token:${GH_TOKEN}@github.com/ente/photos-desktop.git" photos-desktop - git -C photos-desktop commit --allow-empty -m "v${VERSION}" - git -C photos-desktop push - - gh release edit "v${VERSION}" --repo ente/photos-desktop --draft=false --prerelease=false --latest - gh release delete "photos-desktop-v${VERSION}-rc" --yes --cleanup-tag --repo ente/nightly || true - git_push_release_branch ":refs/heads/${release_branch}" - git push origin ":refs/tags/photos-desktop-v${VERSION}-rc" || true - exit 0 - fi - - rc_tag="${APP}-v${VERSION}-rc" - final_tag="${APP}-v${VERSION}" - - if ! rc_is_draft="$(gh release view "${rc_tag}" --json isDraft --jq .isDraft 2>/dev/null)" || [ "${rc_is_draft}" != "true" ]; then - echo "::error::${rc_tag} draft release does not exist" - exit 1 - fi - - branch_version="$(app_version get)" - if [ "${branch_version}" != "${VERSION}" ]; then - echo "::error::${release_branch} is ${branch_version}, expected ${VERSION}" - exit 1 - fi - - release_sha="$(git rev-parse HEAD)" - rc_sha="$(git ls-remote origin "refs/tags/${rc_tag}" | cut -f1)" - - if [ "${rc_sha}" != "${release_sha}" ]; then - echo "::error::${rc_tag} does not point at ${release_branch}" - exit 1 - fi - - git tag "${final_tag}" "${release_sha}" - git push origin "refs/tags/${final_tag}" - gh release edit "${rc_tag}" --tag "${final_tag}" --title "${final_tag}" --target "${release_sha}" --draft=false --prerelease=false --verify-tag - - git push origin ":refs/tags/${rc_tag}" || true - gh release delete "${rc_tag}" --yes --cleanup-tag --repo ente/nightly || true - git_push_release_branch ":refs/heads/${release_branch}" - - sync-docs: - needs: release - if: inputs.action == 'promote' - runs-on: ubuntu-latest - environment: - name: release-branch-push - - steps: - - name: Checkout main - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: main - - - name: Mint App token - id: app_token - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 - with: - client-id: ${{ secrets.ENTE_CI_GH_CLIENT_ID }} - private-key: ${{ secrets.ENTE_CI_GH_APP_PRIVATE_KEY }} - repositories: ente,photos-desktop - permission-contents: write - permission-pull-requests: write - - - name: Open changelog PR - env: - APP: ${{ inputs.app }} - VERSION: ${{ inputs.version }} - GH_TOKEN: ${{ steps.app_token.outputs.token }} - APP_SLUG: ${{ steps.app_token.outputs.app-slug }} - run: | - set -euo pipefail - - git_push_branch() { - git -c http.https://github.com/.extraheader= push \ - "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "$@" - } - - bot_id="$(gh api "/users/${APP_SLUG}[bot]" --jq .id)" - git config user.name "${APP_SLUG}[bot]" - git config user.email "${bot_id}+${APP_SLUG}[bot]@users.noreply.github.com" - - if [ "${APP}" = "photos-desktop" ]; then - RELEASE_BODY="$(gh release view "v${VERSION}" --repo ente/photos-desktop --json body --jq .body)" - else - RELEASE_BODY="$(gh release view "${APP}-v${VERSION}" --json body --jq .body)" - fi - export RELEASE_BODY - - node .github/scripts/sync-help-changelog.mjs "${APP}" "${VERSION}" - if git diff --quiet; then - echo "Help changelog already up to date" - exit 0 - fi - - branch="docs/${APP}-changelog-v${VERSION}" - git switch -c "${branch}" - git commit -am "Update ${APP} help changelog for v${VERSION}" - git_push_branch "${branch}" - gh pr create --base main --head "${branch}" \ - --title "[docs] Update ${APP} help changelog for v${VERSION}" --body "" diff --git a/.github/workflows/auth-build.yml b/.github/workflows/auth-build.yml deleted file mode 100644 index 1cea4a91462..00000000000 --- a/.github/workflows/auth-build.yml +++ /dev/null @@ -1,756 +0,0 @@ -name: "Build (Auth)" - -# See .github/docs/app-release.md - -on: - push: - branches: - - "release/auth-v*" - workflow_dispatch: - schedule: - # Run every weekday at ~3:45 AM IST - - cron: "15 22 * * 0-4" - -permissions: - contents: write - -concurrency: - group: auth-build - cancel-in-progress: false - -jobs: - build-metadata: - runs-on: ubuntu-latest - - outputs: - should_build: ${{ steps.prepare.outputs.should_build }} - commit_sha: ${{ steps.prepare.outputs.commit_sha }} - build_number: ${{ steps.prepare.outputs.build_number }} - release_version: ${{ steps.prepare.outputs.release_version }} - release_tag: ${{ steps.prepare.outputs.release_tag }} - release_title: ${{ steps.prepare.outputs.release_title }} - release_body: ${{ steps.prepare.outputs.release_body }} - release_body_grouped: ${{ steps.prepare.outputs.release_body_grouped }} - has_new_changes: ${{ steps.prepare.outputs.has_new_changes }} - play_store_release_notes: ${{ steps.prepare.outputs.play_store_release_notes }} - artifact_stem: ${{ steps.prepare.outputs.artifact_stem }} - - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ github.ref_name }} - - - name: Prepare release - id: prepare - shell: bash - run: | - set -euo pipefail - - if [[ "${GITHUB_EVENT_NAME}" == "schedule" ]] && git ls-remote --exit-code --heads origin 'release/auth-v*' >/dev/null 2>&1; then - echo "Active Auth release branch found; skipping scheduled nightly" - echo "should_build=false" >> "${GITHUB_OUTPUT}" - exit 0 - fi - - release_version="$(node .github/scripts/flutter-version.mjs auth get)" - - if [[ "${GITHUB_REF_NAME}" == release/auth-v* ]]; then - if [[ "${GITHUB_REF_NAME}" != "release/auth-v${release_version}" ]]; then - echo "::error::Branch ${GITHUB_REF_NAME} does not match Auth version ${release_version}" - exit 1 - fi - build_number="$(node .github/scripts/flutter-version.mjs auth get-build-base)" - release_tag="auth-v${release_version}-rc" - release_title="๐Ÿ”’ Auth release candidate - ${release_version} (build ${build_number})" - else - build_number_base="$(node .github/scripts/flutter-version.mjs auth get-build-base)" - build_number=$((build_number_base + GITHUB_RUN_NUMBER)) - release_tag="auth-v${release_version}-beta" - release_title="๐Ÿ”’ Auth nightly - ${release_version} (build ${build_number})" - fi - - commit_sha="$(git rev-parse HEAD)" - git fetch --force --depth=1 --no-tags origin "refs/tags/${release_tag}:refs/tags/${release_tag}" || true - - echo "should_build=true" >> "${GITHUB_OUTPUT}" - echo "commit_sha=${commit_sha}" >> "${GITHUB_OUTPUT}" - echo "build_number=${build_number}" >> "${GITHUB_OUTPUT}" - echo "release_version=${release_version}" >> "${GITHUB_OUTPUT}" - echo "release_tag=${release_tag}" >> "${GITHUB_OUTPUT}" - echo "release_title=${release_title}" >> "${GITHUB_OUTPUT}" - echo "artifact_stem=ente-${release_tag%-rc}" >> "${GITHUB_OUTPUT}" - node .github/scripts/release-notes.mjs mobile/apps/auth/changes "${release_tag}" >> "${GITHUB_OUTPUT}" - - build-android: - needs: build-metadata - if: needs.build-metadata.outputs.should_build == 'true' - runs-on: ubuntu-latest - environment: - name: production - env: - COMMIT_SHA: ${{ needs.build-metadata.outputs.commit_sha }} - BUILD_NUMBER: ${{ needs.build-metadata.outputs.build_number }} - RELEASE_VERSION: ${{ needs.build-metadata.outputs.release_version }} - RELEASE_TAG: ${{ needs.build-metadata.outputs.release_tag }} - RELEASE_BODY: ${{ needs.build-metadata.outputs.release_body }} - PLAY_STORE_RELEASE_NOTES: ${{ needs.build-metadata.outputs.play_store_release_notes }} - ARTIFACT_STEM: ${{ needs.build-metadata.outputs.artifact_stem }} - - defaults: - run: - working-directory: mobile/apps/auth - - steps: - - name: Checkout code and submodules - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ needs.build-metadata.outputs.commit_sha }} - submodules: recursive - - - name: Setup JDK 17 - uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 - with: - distribution: "temurin" - java-version: "17" - - - name: Install Flutter - uses: ./.github/actions/setup-flutter - - - name: Set build version - run: node ../../../.github/scripts/flutter-version.mjs auth set-build "${RELEASE_VERSION}" "${BUILD_NUMBER}" - - - run: flutter pub get --enforce-lockfile - - - name: Prepare whatsnew - run: | - mkdir -p whatsnew - printf '%s' "${PLAY_STORE_RELEASE_NOTES}" > whatsnew/whatsnew-en-US - - - name: Add keystore - run: printf '%s' "${SIGNING_KEY_AUTH}" | base64 -d > "$RUNNER_TEMP/ente_auth_key.jks" - env: - SIGNING_KEY_AUTH: ${{ secrets.SIGNING_KEY_AUTH }} - - - name: Create artifacts directory - run: mkdir artifacts - - - name: Build independent APK - run: | - flutter build apk --build-name="${RELEASE_VERSION}" --build-number="${BUILD_NUMBER}" --dart-define=cronetHttpNoPlay=true --release --flavor independent - mv build/app/outputs/flutter-apk/app-independent-release.apk "artifacts/${ARTIFACT_STEM}.apk" - env: - SIGNING_KEY_PATH: ${{ runner.temp }}/ente_auth_key.jks - SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS_AUTH }} - SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD_AUTH }} - SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD_AUTH }} - - - name: Build PlayStore AAB - run: | - flutter build appbundle --build-name="${RELEASE_VERSION}" --build-number="${BUILD_NUMBER}" --release --flavor playstore --dart-define=cronetHttpNoPlay=true - env: - SIGNING_KEY_PATH: ${{ runner.temp }}/ente_auth_key.jks - SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS_AUTH }} - SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD_AUTH }} - SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD_AUTH }} - - - name: Generate checksums - run: cd artifacts && sha256sum ente-auth-*.apk > SHA256SUMS-android - - - name: Upload checksum snippet - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: auth-SHA256SUMS-android - path: mobile/apps/auth/artifacts/SHA256SUMS-android - - - name: Mint nightly token - id: nightly_token - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 - with: - client-id: ${{ secrets.ENTE_CI_GH_CLIENT_ID }} - private-key: ${{ secrets.ENTE_CI_GH_APP_PRIVATE_KEY }} - repositories: nightly - permission-contents: write - - - name: Upload Android APK to nightly release - uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0 - with: - repo: nightly - token: ${{ steps.nightly_token.outputs.token }} - tag: ${{ env.RELEASE_TAG }} - name: ${{ env.RELEASE_TAG }} - body: ${{ env.RELEASE_BODY }} - artifacts: "mobile/apps/auth/artifacts/ente-auth-*.apk" - prerelease: true - makeLatest: false - allowUpdates: true - updateOnlyUnreleased: true - - - name: Upload Android APK to RC draft - if: startsWith(github.ref_name, 'release/') - uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0 - with: - tag: ${{ env.RELEASE_TAG }} - name: ${{ env.RELEASE_TAG }} - body: ${{ env.RELEASE_BODY }} - commit: ${{ env.COMMIT_SHA }} - artifacts: "mobile/apps/auth/artifacts/ente-auth-*.apk" - draft: true - allowUpdates: true - updateOnlyUnreleased: true - - - name: Upload AAB to PlayStore - uses: r0adkll/upload-google-play@e738b9dd8f2476ea806d921b64aacd24f34515a5 # v1.1.5 - with: - serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }} - packageName: io.ente.auth - releaseFiles: mobile/apps/auth/build/app/outputs/bundle/playstoreRelease/app-playstore-release.aab - tracks: internal - whatsNewDirectory: mobile/apps/auth/whatsnew - mappingFile: mobile/apps/auth/build/app/outputs/mapping/playstoreRelease/mapping.txt - - build-linux: - needs: build-metadata - if: needs.build-metadata.outputs.should_build == 'true' - runs-on: ubuntu-22.04 - environment: - name: production - env: - COMMIT_SHA: ${{ needs.build-metadata.outputs.commit_sha }} - BUILD_NUMBER: ${{ needs.build-metadata.outputs.build_number }} - RELEASE_VERSION: ${{ needs.build-metadata.outputs.release_version }} - RELEASE_TAG: ${{ needs.build-metadata.outputs.release_tag }} - RELEASE_BODY: ${{ needs.build-metadata.outputs.release_body }} - ARTIFACT_STEM: ${{ needs.build-metadata.outputs.artifact_stem }} - - defaults: - run: - working-directory: mobile/apps/auth - - steps: - - name: Checkout code and submodules - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ needs.build-metadata.outputs.commit_sha }} - submodules: recursive - - - name: Install Flutter - uses: ./.github/actions/setup-flutter - - - name: Set build version - run: node ../../../.github/scripts/flutter-version.mjs auth set-build "${RELEASE_VERSION}" "${BUILD_NUMBER}" - - - run: flutter pub get --enforce-lockfile - - - name: Create artifacts directory - run: mkdir artifacts - - - name: Install dependencies for Linux build - run: | - sudo apt-get update - sudo apt-get install -y libsecret-1-dev libsodium-dev libfuse2 ninja-build libgtk-3-dev dpkg-dev pkg-config rpm patchelf libsqlite3-dev locate libayatana-appindicator3-dev libffi-dev libtiff5 xz-utils libarchive-tools libcurl4-openssl-dev - sudo updatedb --localpaths='/usr/lib/x86_64-linux-gnu /lib/x86_64-linux-gnu' - - - name: Install fpm for RPM packaging - run: sudo gem install fpm - - - name: Install appimagetool - run: | - wget -O appimagetool "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage" - chmod +x appimagetool - mv appimagetool /usr/local/bin/ - - - name: Build Linux app - run: | - flutter config --enable-linux-desktop - flutter build linux --release - ./linux/packaging/build_rpm.sh - mv dist/*/*.rpm "artifacts/${ARTIFACT_STEM}-x86_64.rpm" - dart pub global activate --source git https://github.com/ente/fastforgefork --git-ref 3e94e0555db45d0f969307cfc1ca41a2c7c9d2a5 --git-path packages/fastforge - dart pub global run fastforge:main package --platform=linux --targets=appimage --skip-clean - appimage_path="$(find dist -name '*-*-linux.AppImage' -print -quit)" - ./linux/packaging/post_process_appimage.sh "$appimage_path" - mv "$appimage_path" "artifacts/${ARTIFACT_STEM}-x86_64.AppImage" - dart pub global run fastforge:main package --platform=linux --targets=deb --skip-clean - mv dist/**/*-*-linux.deb "artifacts/${ARTIFACT_STEM}-x86_64.deb" - - - name: Generate checksums - run: cd artifacts && sha256sum ente-auth-*.deb ente-auth-*.rpm ente-auth-*.AppImage > SHA256SUMS-linux - - - name: Upload checksum snippet - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: auth-SHA256SUMS-linux - path: mobile/apps/auth/artifacts/SHA256SUMS-linux - - - name: Mint nightly token - id: nightly_token - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 - with: - client-id: ${{ secrets.ENTE_CI_GH_CLIENT_ID }} - private-key: ${{ secrets.ENTE_CI_GH_APP_PRIVATE_KEY }} - repositories: nightly - permission-contents: write - - - name: Upload Linux artifacts to nightly release - uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0 - with: - repo: nightly - token: ${{ steps.nightly_token.outputs.token }} - tag: ${{ env.RELEASE_TAG }} - name: ${{ env.RELEASE_TAG }} - body: ${{ env.RELEASE_BODY }} - artifacts: "mobile/apps/auth/artifacts/ente-auth-*.deb,mobile/apps/auth/artifacts/ente-auth-*.rpm,mobile/apps/auth/artifacts/ente-auth-*.AppImage" - prerelease: true - makeLatest: false - allowUpdates: true - updateOnlyUnreleased: true - - - name: Upload Linux artifacts to RC draft - if: startsWith(github.ref_name, 'release/') - uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0 - with: - tag: ${{ env.RELEASE_TAG }} - name: ${{ env.RELEASE_TAG }} - body: ${{ env.RELEASE_BODY }} - commit: ${{ env.COMMIT_SHA }} - artifacts: "mobile/apps/auth/artifacts/ente-auth-*.deb,mobile/apps/auth/artifacts/ente-auth-*.rpm,mobile/apps/auth/artifacts/ente-auth-*.AppImage" - draft: true - allowUpdates: true - updateOnlyUnreleased: true - - build-windows: - needs: build-metadata - if: needs.build-metadata.outputs.should_build == 'true' - # VS2022 is required until local_auth_windows builds cleanly on VS2026. - runs-on: windows-2022 - environment: - name: production - env: - COMMIT_SHA: ${{ needs.build-metadata.outputs.commit_sha }} - BUILD_NUMBER: ${{ needs.build-metadata.outputs.build_number }} - RELEASE_VERSION: ${{ needs.build-metadata.outputs.release_version }} - RELEASE_TAG: ${{ needs.build-metadata.outputs.release_tag }} - RELEASE_BODY: ${{ needs.build-metadata.outputs.release_body }} - ARTIFACT_STEM: ${{ needs.build-metadata.outputs.artifact_stem }} - - defaults: - run: - working-directory: mobile/apps/auth - - steps: - - name: Checkout code and submodules - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ needs.build-metadata.outputs.commit_sha }} - submodules: recursive - - - name: Install Flutter - uses: ./.github/actions/setup-flutter - - - name: Set build version - shell: bash - run: node ../../../.github/scripts/flutter-version.mjs auth set-build "${RELEASE_VERSION}" "${BUILD_NUMBER}" - - # Needed for Pub to checkout flutter/packages on Windows. - # https://github.com/dart-lang/pub/issues/3803 - - name: Enable Git long paths - run: git config --global core.longpaths true - - - run: flutter pub get --enforce-lockfile - - - name: Create artifacts directory - run: mkdir artifacts - - - name: Build Windows release - shell: bash - run: | - flutter config --enable-windows-desktop - choco install innosetup -y - flutter build windows --release - - - name: Verify Windows PE allowlist - shell: pwsh - run: | - & ./scripts/verify_windows_pe_allowlist.ps1 ` - -PayloadDir "build/windows/x64/runner/Release" ` - -AllowlistPath "windows/signing/allowed-pe-files.txt" - - - name: Sign Windows payload with Trusted Signing - uses: azure/trusted-signing-action@c7ab2a863ab5f9a846ddb8265964877ef296ee82 # v2.0.0 - with: - azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }} - azure-client-id: ${{ secrets.AZURE_CLIENT_ID }} - azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }} - endpoint: ${{ secrets.AZURE_ENDPOINT }} - signing-account-name: ${{ secrets.AZURE_CODE_SIGNING_NAME }} - certificate-profile-name: ${{ secrets.AZURE_CERT_PROFILE_NAME }} - files-folder: ${{ github.workspace }}/mobile/apps/auth/build/windows/x64/runner/Release - files-folder-filter: exe,dll - files-folder-recurse: true - file-digest: SHA256 - timestamp-rfc3161: http://timestamp.acs.microsoft.com - timestamp-digest: SHA256 - cache-dependencies: false - - - name: Verify signed Windows payload - shell: pwsh - run: ./scripts/verify_windows_authenticode.ps1 -Path "build/windows/x64/runner/Release" - - - name: Build Windows installer - shell: pwsh - run: | - & ./scripts/build_windows_installer.ps1 ` - -SourceDir "build/windows/x64/runner/Release" ` - -InstallerPath "artifacts/${{ env.ARTIFACT_STEM }}-installer.exe" - - - name: Sign Windows installer with Trusted Signing - uses: azure/trusted-signing-action@c7ab2a863ab5f9a846ddb8265964877ef296ee82 # v2.0.0 - with: - azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }} - azure-client-id: ${{ secrets.AZURE_CLIENT_ID }} - azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }} - endpoint: ${{ secrets.AZURE_ENDPOINT }} - signing-account-name: ${{ secrets.AZURE_CODE_SIGNING_NAME }} - certificate-profile-name: ${{ secrets.AZURE_CERT_PROFILE_NAME }} - files: ${{ github.workspace }}/mobile/apps/auth/artifacts/${{ env.ARTIFACT_STEM }}-installer.exe - file-digest: SHA256 - timestamp-rfc3161: http://timestamp.acs.microsoft.com - timestamp-digest: SHA256 - cache-dependencies: false - - - name: Verify signed Windows installer - shell: pwsh - run: ./scripts/verify_windows_authenticode.ps1 -Path "artifacts/${{ env.ARTIFACT_STEM }}-installer.exe" - - - name: Zip Windows EXE and DLLs - shell: pwsh - run: | - Copy-Item -Recurse -Force ` - "build/windows/x64/runner/Release" ` - "${{ env.ARTIFACT_STEM }}-windows" - $zipPath = "artifacts/${{ env.ARTIFACT_STEM }}-windows.zip" - Compress-Archive ` - -Path "${{ env.ARTIFACT_STEM }}-windows" ` - -DestinationPath $zipPath ` - -Force - Add-Type -AssemblyName System.IO.Compression.FileSystem - $zip = [System.IO.Compression.ZipFile]::OpenRead((Resolve-Path $zipPath)) - $zip.Dispose() - - - name: Generate checksums - shell: bash - run: cd artifacts && sha256sum ente-auth-* > SHA256SUMS-windows - - - name: Upload checksum snippet - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: auth-SHA256SUMS-windows - path: mobile/apps/auth/artifacts/SHA256SUMS-windows - - - name: Mint nightly token - id: nightly_token - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 - with: - client-id: ${{ secrets.ENTE_CI_GH_CLIENT_ID }} - private-key: ${{ secrets.ENTE_CI_GH_APP_PRIVATE_KEY }} - repositories: nightly - permission-contents: write - - - name: Upload Windows artifacts to nightly release - uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0 - with: - repo: nightly - token: ${{ steps.nightly_token.outputs.token }} - tag: ${{ env.RELEASE_TAG }} - name: ${{ env.RELEASE_TAG }} - body: ${{ env.RELEASE_BODY }} - artifacts: "mobile/apps/auth/artifacts/ente-auth-*.exe,mobile/apps/auth/artifacts/ente-auth-*.zip" - prerelease: true - makeLatest: false - allowUpdates: true - updateOnlyUnreleased: true - - - name: Upload Windows artifacts to RC draft - if: startsWith(github.ref_name, 'release/') - uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0 - with: - tag: ${{ env.RELEASE_TAG }} - name: ${{ env.RELEASE_TAG }} - body: ${{ env.RELEASE_BODY }} - commit: ${{ env.COMMIT_SHA }} - artifacts: "mobile/apps/auth/artifacts/ente-auth-*.exe,mobile/apps/auth/artifacts/ente-auth-*.zip" - draft: true - allowUpdates: true - updateOnlyUnreleased: true - - build-macos: - needs: build-metadata - if: needs.build-metadata.outputs.should_build == 'true' - runs-on: macos-15 - environment: - name: production - env: - COMMIT_SHA: ${{ needs.build-metadata.outputs.commit_sha }} - BUILD_NUMBER: ${{ needs.build-metadata.outputs.build_number }} - RELEASE_VERSION: ${{ needs.build-metadata.outputs.release_version }} - RELEASE_TAG: ${{ needs.build-metadata.outputs.release_tag }} - RELEASE_BODY: ${{ needs.build-metadata.outputs.release_body }} - ARTIFACT_STEM: ${{ needs.build-metadata.outputs.artifact_stem }} - - defaults: - run: - working-directory: mobile/apps/auth - - steps: - - name: Checkout code and submodules - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ needs.build-metadata.outputs.commit_sha }} - submodules: recursive - - - name: Install Flutter - uses: ./.github/actions/setup-flutter - - - name: Set build version - run: node ../../../.github/scripts/flutter-version.mjs auth set-build "${RELEASE_VERSION}" "${BUILD_NUMBER}" - - - run: flutter pub get --enforce-lockfile - - - name: Install code signing dependencies - run: pip3 install codemagic-cli-tools --break-system-packages - - - name: Add provisioning profiles - run: | - PROFILES_HOME="$HOME/Library/MobileDevice/Provisioning Profiles" - mkdir -p "$PROFILES_HOME" - printf '%s' "${CM_PROVISIONING_PROFILE}" | base64 --decode > "$PROFILES_HOME/$(uuidgen).provisionprofile" - env: - CM_PROVISIONING_PROFILE: ${{ secrets.AUTH_MACOS_DEVELOPER_ID_PROVISION_PROFILE_BASE64 }} - - - name: Add certificates - run: | - CERTIFICATE_PATH="$RUNNER_TEMP/build_certificate.p12" - printf '%s' "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o "$CERTIFICATE_PATH" - keychain initialize - keychain add-certificates --certificate "$CERTIFICATE_PATH" --certificate-password "$P12_PASSWORD" - xcode-project use-profiles --project=macos/**/*.xcodeproj - - signing_settings="$RUNNER_TEMP/auth-macos-ci-signing.xcconfig" - printf '%s\n' \ - 'CODE_SIGN_IDENTITY = Developer ID Application' \ - 'CODE_SIGN_STYLE = Manual' \ - > "$signing_settings" - echo "XCODE_XCCONFIG_FILE=$signing_settings" >> "$GITHUB_ENV" - env: - BUILD_CERTIFICATE_BASE64: ${{ secrets.MAC_OS_CERTIFICATE }} - P12_PASSWORD: ${{ secrets.MAC_OS_CERTIFICATE_PASSWORD }} - - - name: Create artifacts directory - run: mkdir artifacts - - - name: Build macOS DMG - run: | - flutter config --enable-macos-desktop - flutter build macos --release - ./macos/packaging/dmg/make-dmg.sh "build/macos/Build/Products/Release/Ente Auth.app" "artifacts/${ARTIFACT_STEM}.dmg" - - - name: Code sign DMG - run: | - CERT_NAME=$(security find-identity -v -p codesigning | awk -F'"' '/Developer ID Application/ { print $2; exit }') - codesign --force --timestamp --sign "$CERT_NAME" --options runtime "artifacts/${ARTIFACT_STEM}.dmg" - codesign --verify --verbose=4 "artifacts/${ARTIFACT_STEM}.dmg" - - - name: Notarize and staple DMG - run: | - xcrun notarytool submit "artifacts/${ARTIFACT_STEM}.dmg" \ - --wait \ - --apple-id "$APPLE_ID" \ - --password "$APPLE_PASSWORD" \ - --team-id "$APPLE_TEAM_ID" - xcrun stapler staple "artifacts/${ARTIFACT_STEM}.dmg" - env: - APPLE_ID: ${{ secrets.APPLE_ID }} - APPLE_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - - - name: Generate checksums - run: cd artifacts && shasum -a 256 ente-auth-* > SHA256SUMS-macos - - - name: Upload checksum snippet - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: auth-SHA256SUMS-macos - path: mobile/apps/auth/artifacts/SHA256SUMS-macos - - - name: Mint nightly token - id: nightly_token - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 - with: - client-id: ${{ secrets.ENTE_CI_GH_CLIENT_ID }} - private-key: ${{ secrets.ENTE_CI_GH_APP_PRIVATE_KEY }} - repositories: nightly - permission-contents: write - - - name: Upload macOS artifacts to nightly release - uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0 - with: - repo: nightly - token: ${{ steps.nightly_token.outputs.token }} - tag: ${{ env.RELEASE_TAG }} - name: ${{ env.RELEASE_TAG }} - body: ${{ env.RELEASE_BODY }} - artifacts: "mobile/apps/auth/artifacts/ente-auth-*.dmg" - prerelease: true - makeLatest: false - allowUpdates: true - updateOnlyUnreleased: true - - - name: Upload macOS artifacts to RC draft - if: startsWith(github.ref_name, 'release/') - uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0 - with: - tag: ${{ env.RELEASE_TAG }} - name: ${{ env.RELEASE_TAG }} - body: ${{ env.RELEASE_BODY }} - commit: ${{ env.COMMIT_SHA }} - artifacts: "mobile/apps/auth/artifacts/ente-auth-*.dmg" - draft: true - allowUpdates: true - updateOnlyUnreleased: true - - build-ios: - needs: build-metadata - if: needs.build-metadata.outputs.should_build == 'true' - runs-on: macos-26 - environment: - name: production - env: - BUILD_NUMBER: ${{ needs.build-metadata.outputs.build_number }} - RELEASE_VERSION: ${{ needs.build-metadata.outputs.release_version }} - - defaults: - run: - working-directory: mobile/apps/auth - - steps: - - name: Checkout code and submodules - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ needs.build-metadata.outputs.commit_sha }} - submodules: recursive - - - name: Install Flutter - uses: ./.github/actions/setup-flutter - - - name: Set build version - run: node ../../../.github/scripts/flutter-version.mjs auth set-build "${RELEASE_VERSION}" "${BUILD_NUMBER}" - - - run: flutter pub get --enforce-lockfile - - - name: Install iOS pods - run: pod install --deployment - working-directory: mobile/apps/auth/ios - - - name: Import Apple Distribution certificate - uses: Apple-Actions/import-codesign-certs@5142e029c445c10ffc7149d172e540235a065466 # v7.0.0 - with: - p12-file-base64: ${{ secrets.APPLE_DISTRIBUTION_CERT_BASE64 }} - p12-password: ${{ secrets.APPLE_DISTRIBUTION_CERT_PASSWORD }} - - - name: Archive and upload Auth to TestFlight - env: - ASC_API_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_API_KEY_BASE64 }} - ASC_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }} - ASC_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - run: | - key_path="${RUNNER_TEMP}/AuthKey_${ASC_KEY_ID}.p8" - printf '%s' "${ASC_API_KEY_BASE64}" | base64 --decode > "${key_path}" - - xcodebuild archive \ - -workspace ios/Runner.xcworkspace \ - -scheme Runner \ - -configuration Release \ - -destination 'generic/platform=iOS' \ - -archivePath build/ios/archive/Runner.xcarchive \ - -allowProvisioningUpdates \ - -authenticationKeyPath "${key_path}" \ - -authenticationKeyID "${ASC_KEY_ID}" \ - -authenticationKeyIssuerID "${ASC_ISSUER_ID}" \ - CODE_SIGN_STYLE=Manual \ - CODE_SIGN_IDENTITY="Apple Distribution" \ - DEVELOPMENT_TEAM="${APPLE_TEAM_ID}" \ - APP_STORE_PROFILE_SPECIFIER="Auth App Store" - - xcodebuild -exportArchive \ - -archivePath build/ios/archive/Runner.xcarchive \ - -exportPath build/ios/export \ - -exportOptionsPlist scripts/ExportOptions-AppStore-CI.plist \ - -allowProvisioningUpdates \ - -authenticationKeyPath "${key_path}" \ - -authenticationKeyID "${ASC_KEY_ID}" \ - -authenticationKeyIssuerID "${ASC_ISSUER_ID}" - - finish-build: - needs: [build-metadata, build-android, build-linux, build-windows, build-macos, build-ios] - if: needs.build-metadata.outputs.should_build == 'true' - runs-on: ubuntu-latest - environment: - name: production - - steps: - - name: Checkout built commit - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ needs.build-metadata.outputs.commit_sha }} - - - name: Download checksum snippets - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - pattern: auth-SHA256SUMS-* - path: checksums - merge-multiple: true - - - name: Combine checksums - run: sort -k2 checksums/SHA256SUMS-* > SHA256SUMS - - - name: Point release tag at the successfully built commit - env: - COMMIT_SHA: ${{ needs.build-metadata.outputs.commit_sha }} - RELEASE_TAG: ${{ needs.build-metadata.outputs.release_tag }} - run: | - git tag --force "${RELEASE_TAG}" "${COMMIT_SHA}" - git push --force origin "refs/tags/${RELEASE_TAG}" - - - name: Mint nightly token - id: nightly_token - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 - with: - client-id: ${{ secrets.ENTE_CI_GH_CLIENT_ID }} - private-key: ${{ secrets.ENTE_CI_GH_APP_PRIVATE_KEY }} - repositories: nightly - permission-contents: write - - - name: Upload checksum to nightly release - env: - GH_TOKEN: ${{ steps.nightly_token.outputs.token }} - RELEASE_TAG: ${{ needs.build-metadata.outputs.release_tag }} - run: gh release upload "${RELEASE_TAG}" SHA256SUMS --clobber --repo ente/nightly - - - name: Upload checksum to RC draft - if: startsWith(github.ref_name, 'release/') - env: - GH_TOKEN: ${{ github.token }} - RELEASE_TAG: ${{ needs.build-metadata.outputs.release_tag }} - run: gh release upload "${RELEASE_TAG}" SHA256SUMS --clobber - - - name: Notify Discord - if: github.event_name != 'schedule' || needs.build-metadata.outputs.has_new_changes == 'true' - continue-on-error: true - env: - DISCORD_WEBHOOK: ${{ secrets.DISCORD_INTERNAL_RELEASE_WEBHOOK }} - RELEASE_TITLE: ${{ needs.build-metadata.outputs.release_title }} - RELEASE_BODY_GROUPED: ${{ needs.build-metadata.outputs.release_body_grouped }} - RELEASE_TAG: ${{ needs.build-metadata.outputs.release_tag }} - run: node .github/scripts/release-notify.mjs auth "${RELEASE_TAG}" diff --git a/.github/workflows/cli-release.yml b/.github/workflows/cli-release.yml deleted file mode 100644 index 9be47825ed9..00000000000 --- a/.github/workflows/cli-release.yml +++ /dev/null @@ -1,108 +0,0 @@ -name: "Release (CLI)" - -on: - push: - # Run when a tag matching the pattern "cli-v*"" is pushed - # - # Tip: to test this workflow, push at tag with a pre-release version, - # e.g. `cli-v1.2.3-test`, where 1.2.3 is the expected version number of - # the next release that'll go out. - # - # See: [Note: Testing release workflows that are triggered by tags] - tags: - - "cli-v*" - -permissions: - contents: read - -jobs: - build: - runs-on: ubuntu-latest - - strategy: - matrix: - goos: [linux, windows, darwin] - goarch: ["386", amd64, arm64] - exclude: - - goarch: "386" - goos: darwin - - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - name: Setup go - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 - with: - go-version-file: cli/go.mod - cache: false - - - name: Build release asset - id: build - env: - CGO_ENABLED: 0 - GOARCH: ${{ matrix.goarch }} - GOOS: ${{ matrix.goos }} - run: | - set -euo pipefail - - asset="ente-${GITHUB_REF_NAME}-${GOOS}-${GOARCH}" - binary="ente" - dist="$RUNNER_TEMP/cli-release" - version="${GITHUB_REF_NAME#cli-}" - - if [ "$GOOS" = windows ]; then - binary="ente.exe" - fi - - mkdir -p "$dist" - go build -C cli -trimpath -ldflags "-X main.AppVersion=$version -s -w" -o "$dist/$binary" - - cd "$dist" - if [ "$GOOS" = windows ]; then - asset="$asset.zip" - zip -vr "$asset" "$binary" - else - asset="$asset.tar.gz" - tar cvfz "$asset" "$binary" - fi - - echo "asset=$asset" >> "$GITHUB_OUTPUT" - echo "asset_path=$dist/$asset" >> "$GITHUB_OUTPUT" - - - name: Upload release asset - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: ${{ steps.build.outputs.asset }} - path: ${{ steps.build.outputs.asset_path }} - if-no-files-found: error - compression-level: 0 - - release: - runs-on: ubuntu-latest - needs: build - permissions: - contents: write - steps: - - name: Download release assets - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - path: ${{ runner.temp }}/cli-release-assets - pattern: ente-${{ github.ref_name }}-* - merge-multiple: true - - - name: Create draft release - env: - GH_REPO: ${{ github.repository }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -euo pipefail - - cd "$RUNNER_TEMP/cli-release-assets" - sha256sum * | sort -k2 > SHA256SUMS - gh release create "$GITHUB_REF_NAME" \ - --draft \ - --notes "" \ - --title "$GITHUB_REF_NAME" \ - --verify-tag \ - * diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 288450689e7..ab995b7df79 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,53 +1,63 @@ -# The only reason we're making a custom CodeQL configuration is because the -# default setup does not have an option to run daily / weekly instead of per -# push, which is too slow. -# -# In the future, it might become faster. Then just delete this file, and revert -# to the default configuration. -# -# References: -# https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/ - -name: "CodeQL" +name: CodeQL (GitHub Actions) on: - workflow_dispatch: # Allow running manually - schedule: - - cron: "22 1 * * 1" + pull_request: + schedule: + - cron: "22 1 * * 1" + workflow_dispatch: -jobs: - analyze: - name: Analyze (${{ matrix.language }}) - runs-on: 'ubuntu-latest' - permissions: - contents: read - # Required for all workflows. - security-events: write - # Required to fetch internal or private CodeQL packs. - packages: read +permissions: + contents: read + pull-requests: read + security-events: write - strategy: - fail-fast: false - matrix: - include: - - language: actions - build-mode: none - - language: go - build-mode: autobuild - - language: javascript-typescript - build-mode: none +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + analyze-actions: + name: Actions CodeQL gate + if: github.repository == 'vanton1/ente' + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - name: Detect workflow-code changes + id: paths + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + if (context.eventName !== "pull_request") { + core.setOutput("relevant", "true"); + return; + } + const files = await github.paginate(github.rest.pulls.listFiles, { + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + per_page: 100, + }); + const relevant = files.some(({ filename }) => + filename.startsWith(".github/actions/") || + filename.startsWith(".github/workflows/") + ); + core.setOutput("relevant", relevant ? "true" : "false"); - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - name: Checkout workflow source + if: steps.paths.outputs.relevant == 'true' + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false - - name: Initialize CodeQL - uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 - with: - languages: ${{ matrix.language }} - build-mode: ${{ matrix.build-mode }} + - name: Initialize CodeQL for GitHub Actions + if: steps.paths.outputs.relevant == 'true' + uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 + with: + languages: actions + build-mode: none - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 - with: - category: "/language:${{matrix.language}}" + - name: Analyze GitHub Actions + if: steps.paths.outputs.relevant == 'true' + uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 + with: + category: /language:actions diff --git a/.github/workflows/copycat-db-release.yml b/.github/workflows/copycat-db-release.yml deleted file mode 100644 index 62935b04c37..00000000000 --- a/.github/workflows/copycat-db-release.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: "Release (copycat-db)" - -on: - workflow_dispatch: - -permissions: - contents: read - -jobs: - build: - runs-on: ubuntu-latest - environment: production - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - name: Log in to Docker registry - env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - run: printf '%s' "${DOCKER_PASSWORD}" | docker login rg.fr-par.scw.cloud --username "${DOCKER_USERNAME}" --password-stdin - - - name: Build and push image - run: | - docker buildx build --push \ - --file infra/copycat-db/Dockerfile \ - --build-arg GIT_COMMIT="${GITHUB_SHA}" \ - --tag rg.fr-par.scw.cloud/ente/copycat-db:${GITHUB_SHA} \ - --tag rg.fr-par.scw.cloud/ente/copycat-db:latest \ - infra/copycat-db diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 0d069733cad..8271dc74d19 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -1,29 +1,25 @@ -name: "Dependency review" +name: Dependency review # Fail if a dependency added or updated by a PR has a known security advisory. on: - pull_request: - paths: - - ".github/workflows/dependency-review.yml" - - "**/Cargo.lock" - - "**/Cargo.toml" - - "**/go.mod" - - "**/go.sum" - - "**/package-lock.json" - - "**/package.json" - - "**/pubspec.lock" - - "**/pubspec.yaml" - - "**/uv.lock" + pull_request: permissions: - contents: read + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: - dependency-review: - runs-on: ubuntu-latest - steps: - - name: Dependency review - uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 - with: - fail-on-scopes: runtime, development, unknown + dependency-review: + name: Dependency review gate + if: github.repository == 'vanton1/ente' + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Reject vulnerable dependency changes + uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 + with: + fail-on-scopes: runtime, development, unknown diff --git a/.github/workflows/desktop-lint.yml b/.github/workflows/desktop-lint.yml deleted file mode 100644 index 7b5723e55ec..00000000000 --- a/.github/workflows/desktop-lint.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: "Lint (Photos desktop)" - -on: - pull_request: - paths: - - ".github/workflows/desktop-lint.yml" - - "desktop/**" - -permissions: - contents: read - -jobs: - lint: - runs-on: ubuntu-latest - defaults: - run: - working-directory: desktop - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - name: Setup node and enable npm caching - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: 24 - cache: "npm" - cache-dependency-path: "desktop/package-lock.json" - - - run: npm ci - - - run: npm run lint diff --git a/.github/workflows/docs-deploy-redirect.yml b/.github/workflows/docs-deploy-redirect.yml deleted file mode 100644 index 511ca6d9614..00000000000 --- a/.github/workflows/docs-deploy-redirect.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: "Deploy (help.ente.io redirect)" - -on: - push: - branches: [main] - paths: - - "infra/services/help-redir/**" - workflow_dispatch: - -permissions: - contents: read - -jobs: - deploy: - runs-on: ubuntu-latest - environment: production - - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - name: Deploy redirect to help.ente.io - uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0 - with: - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - # Deploy the redirect file to the existing ente project's help branch - # This will replace the current help.ente.io content with just redirects - command: pages deploy --project-name=ente --commit-dirty=true --branch=help infra/services/help-redir diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml deleted file mode 100644 index 43957a69b16..00000000000 --- a/.github/workflows/docs-deploy.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: "Deploy (Docs)" - -on: - push: - branches: [main] - paths: - - ".github/workflows/docs-deploy.yml" - - "docs/**" - workflow_dispatch: - -permissions: - contents: read - -jobs: - deploy: - runs-on: ubuntu-latest - environment: production - - defaults: - run: - working-directory: docs - - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - name: Setup node and enable npm caching - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: 24 - cache: "npm" - cache-dependency-path: "docs/package-lock.json" - - - run: npm ci - - - run: npm audit --audit-level=critical - - # Will create docs/.vitepress/dist - - run: npm run build - - - name: Prepare deployment - run: | - mkdir -p deploy - mv docs/.vitepress/dist deploy/help - - - name: Publish - uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0 - with: - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - command: pages deploy --project-name=ente --commit-dirty=true --branch=help-content docs/deploy diff --git a/.github/workflows/docs-verify-build.yml b/.github/workflows/docs-verify-build.yml deleted file mode 100644 index ac07685f5c4..00000000000 --- a/.github/workflows/docs-verify-build.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: "Verify build (Docs)" - -# Preflight build of docs. This allows us to ensure that npm run build is -# succeeding before we merge the PR into main. - -on: - # Run on every pull request (open or push to it) that changes docs/ - pull_request: - paths: - - "docs/**" - - ".github/workflows/docs-verify-build.yml" - -permissions: - contents: read - -jobs: - verify-build: - runs-on: ubuntu-latest - - defaults: - run: - working-directory: docs - - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - name: Setup node and enable npm caching - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: 24 - cache: "npm" - cache-dependency-path: "docs/package-lock.json" - - - run: npm ci - - - run: npm audit --audit-level=critical - - - run: npm run build diff --git a/.github/workflows/ensu-android-build.yml b/.github/workflows/ensu-android-build.yml deleted file mode 100644 index 4592411c459..00000000000 --- a/.github/workflows/ensu-android-build.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: "Build (Ensu Android)" - -on: - pull_request: - paths: - - "mobile/native/android/apps/ensu/**" - - "mobile/native/android/apps/ensu/rust/**" - - "rust/apps/codegen/**" - - "rust/crates/core/**" - - "rust/crates/ensu/**" - - "rust/bindings/uniffi/**" - - "rust/.cargo/config.toml" - - "rust/Cargo.toml" - - "rust/Cargo.lock" - - ".github/workflows/ensu-android-build.yml" - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - # Restore-only; primed on main by warm-caches.yml. - - name: Restore Rust cache - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - with: - workspaces: rust - shared-key: ensu-android - save-if: false - - # cargo codegen native builds the uniffi library for the host, which - # on Linux enables the llama.cpp Vulkan backend. - - name: Install Vulkan build dependencies - run: | - sudo apt-get update - sudo apt-get install -y libvulkan-dev glslc - - - run: cargo codegen native - working-directory: rust - - - name: Build Ensu - working-directory: mobile/native/android/apps/ensu - run: ./gradlew :app:assembleDebug diff --git a/.github/workflows/ensu-build.yml b/.github/workflows/ensu-build.yml deleted file mode 100644 index 0eb33b7466c..00000000000 --- a/.github/workflows/ensu-build.yml +++ /dev/null @@ -1,507 +0,0 @@ -name: "Build (Ensu)" - -# See .github/docs/app-release.md - -on: - push: - branches: - - "release/ensu-v*" - workflow_dispatch: - schedule: - # Run every weekday at ~5:45 AM IST - - cron: "15 0 * * 1-5" - -env: - NDK_VERSION: "27.3.13750724" - -permissions: - contents: write - -concurrency: - group: ensu-build - cancel-in-progress: false - -jobs: - build-metadata: - runs-on: ubuntu-latest - - outputs: - should_build: ${{ steps.prepare.outputs.should_build }} - commit_sha: ${{ steps.prepare.outputs.commit_sha }} - build_number: ${{ steps.prepare.outputs.build_number }} - release_tag: ${{ steps.prepare.outputs.release_tag }} - release_title: ${{ steps.prepare.outputs.release_title }} - release_body: ${{ steps.prepare.outputs.release_body }} - release_body_grouped: ${{ steps.prepare.outputs.release_body_grouped }} - has_new_changes: ${{ steps.prepare.outputs.has_new_changes }} - play_store_release_notes: ${{ steps.prepare.outputs.play_store_release_notes }} - android_apk_name: ${{ steps.prepare.outputs.android_apk_name }} - - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ github.ref_name }} - - - name: Prepare release - id: prepare - shell: bash - run: | - set -euo pipefail - - if [[ "${GITHUB_EVENT_NAME}" == "schedule" ]] && git ls-remote --exit-code --heads origin 'release/ensu-v*' >/dev/null 2>&1; then - echo "Active Ensu release branch found; skipping scheduled nightly" - echo "should_build=false" >> "${GITHUB_OUTPUT}" - exit 0 - fi - - version="$(node .github/scripts/ensu-version.mjs get)" - release_version="${version%-beta}" - - if [[ "${GITHUB_REF_NAME}" == release/ensu-v* ]]; then - if [[ "${version}" != "${release_version}" ]]; then - echo "::error::${GITHUB_REF_NAME} is ${version}, expected ${release_version}" - exit 1 - fi - if [[ "${GITHUB_REF_NAME}" != "release/ensu-v${release_version}" ]]; then - echo "::error::Branch ${GITHUB_REF_NAME} does not match Ensu version ${release_version}" - exit 1 - fi - build_number="$(node .github/scripts/ensu-version.mjs get-build-base)" - release_tag="ensu-v${release_version}-rc" - release_title="๐Ÿค– Ensu release candidate ยท ${release_version} (build ${build_number})" - else - if [[ "${version}" != "${release_version}-beta" ]]; then - echo "::error::${GITHUB_REF_NAME} is ${version}, expected ${release_version}-beta" - exit 1 - fi - build_number_base="$(node .github/scripts/ensu-version.mjs get-build-base)" - build_number=$((build_number_base + GITHUB_RUN_NUMBER)) - release_tag="ensu-v${release_version}-beta" - release_title="๐Ÿค– Ensu nightly ยท ${version} (build ${build_number})" - fi - - commit_sha="$(git rev-parse HEAD)" - git fetch --force --depth=1 --no-tags origin "refs/tags/${release_tag}:refs/tags/${release_tag}" || true - - echo "should_build=true" >> "${GITHUB_OUTPUT}" - echo "commit_sha=${commit_sha}" >> "${GITHUB_OUTPUT}" - echo "build_number=${build_number}" >> "${GITHUB_OUTPUT}" - echo "release_tag=${release_tag}" >> "${GITHUB_OUTPUT}" - echo "release_title=${release_title}" >> "${GITHUB_OUTPUT}" - echo "android_apk_name=${release_tag%-rc}.apk" >> "${GITHUB_OUTPUT}" - node .github/scripts/release-notes.mjs rust/apps/ensu/changes "${release_tag}" >> "${GITHUB_OUTPUT}" - - build-desktop: - needs: build-metadata - if: needs.build-metadata.outputs.should_build == 'true' - environment: - name: production - strategy: - fail-fast: false - matrix: - platform: - # Tauri docs recommend building Linux release bundles on the - # oldest supported runtime OS with WebKitGTK 4.1, so that - # AppImage/deb/rpm artifacts keep a low glibc baseline. - - os: ubuntu-22.04 - rust-target: x86_64-unknown-linux-gnu - bundle-targets: appimage,deb,rpm - # A single universal (arm64 + x86_64) build, so that there - # is one dmg that works on all Macs, and existing installs - # of either architecture converge onto it via the updater. - - os: macos-latest - rust-target: universal-apple-darwin - bundle-targets: app,dmg - - os: windows-latest - rust-target: x86_64-pc-windows-msvc - bundle-targets: nsis - - runs-on: ${{ matrix.platform.os }} - env: - COMMIT_SHA: ${{ needs.build-metadata.outputs.commit_sha }} - RELEASE_TAG: ${{ needs.build-metadata.outputs.release_tag }} - RELEASE_BODY: ${{ needs.build-metadata.outputs.release_body }} - - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ needs.build-metadata.outputs.commit_sha }} - - - name: Setup Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: 24 - cache: "npm" - cache-dependency-path: "web/package-lock.json" - - - name: Install dependencies (Ubuntu only) - if: matrix.platform.os == 'ubuntu-22.04' - run: | - sudo apt-get update - sudo apt-get install -y libwebkit2gtk-4.1-dev libsoup-3.0-dev libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev - # The llama.cpp Vulkan backend needs the Vulkan loader and the - # glslc shader compiler at build time. Ubuntu 22.04 has no - # glslc package, so use the LunarG SDK repository (which - # llama.cpp's own CI also uses). - sudo wget -qO /usr/share/keyrings/lunarg.asc https://packages.lunarg.com/lunarg-signing-key-pub.asc - echo "deb [signed-by=/usr/share/keyrings/lunarg.asc] https://packages.lunarg.com/vulkan jammy main" | sudo tee /etc/apt/sources.list.d/lunarg-vulkan-jammy.list >/dev/null - sudo apt-get update - sudo apt-get install -y vulkan-sdk - - - name: Install macOS Rust targets - if: matrix.platform.rust-target == 'universal-apple-darwin' - run: rustup target add x86_64-apple-darwin aarch64-apple-darwin - - - name: Install web dependencies - working-directory: web - run: npm ci - - - name: Build WASM - working-directory: web - run: npm run build:wasm - - - name: Install Tauri dependencies - run: npm ci --prefix rust/apps/ensu - - - name: Check Tauri updater public key - if: vars.ENSU_TAURI_UPDATER_PUBKEY == '' - run: exit 1 - - - name: Write Tauri config override (updater) - if: matrix.platform.os != 'windows-latest' - run: | - cat > rust/apps/ensu/tauri.ci.conf.json <<'EOF' - { - "plugins": { - "updater": { - "pubkey": ${{ toJSON(vars.ENSU_TAURI_UPDATER_PUBKEY) }} - } - }, - "bundle": { - "createUpdaterArtifacts": "v1Compatible" - } - } - EOF - - - name: Write Tauri config override (updater + Windows signing) - if: matrix.platform.os == 'windows-latest' - shell: bash - run: | - test "${{ secrets.AZURE_ENDPOINT != '' && secrets.AZURE_CODE_SIGNING_NAME != '' && secrets.AZURE_CERT_PROFILE_NAME != '' }}" = true - cargo install --locked artifact-signing-cli --version 0.11.0 - - artifact_signing_cli="${CARGO_HOME:-$HOME/.cargo}/bin/artifact-signing-cli.exe" - test -f "$artifact_signing_cli" || { echo "::error::artifact-signing-cli not found at $artifact_signing_cli"; exit 1; } - artifact_signing_cli="$(cygpath -w "$artifact_signing_cli")" - - SIGNTOOL_PATH=$(powershell -NoProfile -Command "Get-ChildItem 'C:\\Program Files (x86)\\Windows Kits\\10\\bin\\*\\x64\\signtool.exe' -ErrorAction Stop | Sort-Object FullName -Descending | Select-Object -First 1 -ExpandProperty FullName" | tr -d '\r') - echo "SIGNTOOL_PATH=$SIGNTOOL_PATH" >> $GITHUB_ENV - - jq -n \ - --arg signer "$artifact_signing_cli" \ - '{ - plugins: { - updater: { pubkey: ${{ toJSON(vars.ENSU_TAURI_UPDATER_PUBKEY) }} } - }, - bundle: { - createUpdaterArtifacts: "v1Compatible", - windows: { - signCommand: { - cmd: $signer, - args: [ - "-e", - ${{ toJSON(secrets.AZURE_ENDPOINT) }}, - "-a", - ${{ toJSON(secrets.AZURE_CODE_SIGNING_NAME) }}, - "-c", - ${{ toJSON(secrets.AZURE_CERT_PROFILE_NAME) }}, - "-d", - "Ensu", - "%1" - ] - } - } - } - }' \ - > rust/apps/ensu/tauri.ci.conf.json - - - name: Mint nightly token - id: nightly_token - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 - with: - client-id: ${{ secrets.ENTE_CI_GH_CLIENT_ID }} - private-key: ${{ secrets.ENTE_CI_GH_APP_PRIVATE_KEY }} - repositories: nightly - permission-contents: write - - - name: Build and release Tauri app - id: tauri - uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 # v0.6.2 - env: - GITHUB_TOKEN: ${{ steps.nightly_token.outputs.token }} - TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.ENSU_TAURI_SIGNING_PRIVATE_KEY }} - TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.ENSU_TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} - APPLE_CERTIFICATE: ${{ secrets.MAC_OS_CERTIFICATE }} - APPLE_CERTIFICATE_PASSWORD: ${{ secrets.MAC_OS_CERTIFICATE_PASSWORD }} - APPLE_SIGNING_IDENTITY: "Developer ID Application" - KEYCHAIN_PASSWORD: ${{ secrets.MAC_OS_CERTIFICATE_PASSWORD }} - APPLE_ID: ${{ secrets.APPLE_ID }} - APPLE_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} - AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} - AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} - SIGNTOOL_PATH: ${{ env.SIGNTOOL_PATH }} - with: - projectPath: rust/apps/ensu - repo: nightly - tagName: ${{ env.RELEASE_TAG }} - releaseName: ${{ env.RELEASE_TAG }} - releaseBody: ${{ env.RELEASE_BODY }} - # Tag off nightly's default branch; the build commit isn't in that repo. - releaseCommitish: main - releaseDraft: false - prerelease: true - includeDebug: false - includeRelease: true - includeUpdaterJson: false - args: ${{ format('--target {0} --bundles {1} --config tauri.ci.conf.json', matrix.platform.rust-target, matrix.platform.bundle-targets) }} - - # tauri-action emits JSON; ncipollo expects comma-delimited artifact paths. - - name: Collect RC artifact list - if: startsWith(github.ref_name, 'release/') - id: rc_artifacts - shell: bash - env: - ARTIFACT_PATHS: ${{ steps.tauri.outputs.artifactPaths }} - run: | - list="$(jq -rn 'env.ARTIFACT_PATHS | fromjson | join(",")')" - printf 'list=%s\n' "${list}" >> "${GITHUB_OUTPUT}" - - - name: Upload desktop artifacts to RC draft - if: startsWith(github.ref_name, 'release/') - uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0 - with: - tag: ${{ env.RELEASE_TAG }} - name: ${{ env.RELEASE_TAG }} - body: ${{ env.RELEASE_BODY }} - commit: ${{ env.COMMIT_SHA }} - artifacts: ${{ steps.rc_artifacts.outputs.list }} - draft: true - allowUpdates: true - updateOnlyUnreleased: true - - build-android: - needs: build-metadata - if: needs.build-metadata.outputs.should_build == 'true' - runs-on: ubuntu-latest - environment: - name: production - env: - COMMIT_SHA: ${{ needs.build-metadata.outputs.commit_sha }} - BUILD_NUMBER: ${{ needs.build-metadata.outputs.build_number }} - RELEASE_TAG: ${{ needs.build-metadata.outputs.release_tag }} - RELEASE_BODY: ${{ needs.build-metadata.outputs.release_body }} - PLAY_STORE_RELEASE_NOTES: ${{ needs.build-metadata.outputs.play_store_release_notes }} - ANDROID_APK_NAME: ${{ needs.build-metadata.outputs.android_apk_name }} - - defaults: - run: - working-directory: mobile/native/android/apps/ensu - - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ needs.build-metadata.outputs.commit_sha }} - - - name: Setup JDK 17 - uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 - with: - distribution: "temurin" - java-version: "17" - - - name: Setup Android SDK - uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1 - - - name: Install NDK - run: | - echo "y" | sdkmanager --install "ndk;${{ env.NDK_VERSION }}" - echo "ANDROID_NDK_HOME=$ANDROID_HOME/ndk/${{ env.NDK_VERSION }}" >> $GITHUB_ENV - - # cargo codegen native builds the uniffi library for the host, which - # on Linux enables the llama.cpp Vulkan backend. - - name: Install Vulkan build dependencies - run: | - sudo apt-get update - sudo apt-get install -y libvulkan-dev glslc - - - run: cargo codegen native - working-directory: rust - - - name: Setup signing keys - env: - SIGNING_KEY_ENSU: ${{ secrets.SIGNING_KEY_ENSU }} - SIGNING_STORE_PASSWORD_ENSU: ${{ secrets.SIGNING_STORE_PASSWORD_ENSU }} - SIGNING_KEY_ALIAS_ENSU: ${{ secrets.SIGNING_KEY_ALIAS_ENSU }} - SIGNING_KEY_PASSWORD_ENSU: ${{ secrets.SIGNING_KEY_PASSWORD_ENSU }} - run: | - KEYSTORE_PATH=$(pwd)/ensu-release.keystore - printf '%s' "${SIGNING_KEY_ENSU}" | base64 -d > "$KEYSTORE_PATH" - cat > key.properties << EOF - storeFile=$KEYSTORE_PATH - storePassword=${SIGNING_STORE_PASSWORD_ENSU} - keyAlias=${SIGNING_KEY_ALIAS_ENSU} - keyPassword=${SIGNING_KEY_PASSWORD_ENSU} - EOF - - - name: Build signed Android artifacts - run: ./gradlew -PversionCode="${BUILD_NUMBER}" :app:assembleRelease :app:bundleRelease - - - name: Prepare whatsnew - run: | - mkdir -p whatsnew - printf '%s' "${PLAY_STORE_RELEASE_NOTES}" > whatsnew/whatsnew-en-US - - - name: Prepare Android APK for GitHub release - run: | - mkdir -p artifacts - cp app/build/outputs/apk/release/app-release.apk "artifacts/${ANDROID_APK_NAME}" - - - name: Mint nightly token - id: nightly_token - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 - with: - client-id: ${{ secrets.ENTE_CI_GH_CLIENT_ID }} - private-key: ${{ secrets.ENTE_CI_GH_APP_PRIVATE_KEY }} - repositories: nightly - permission-contents: write - - - name: Upload Android APK to nightly release - uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0 - with: - repo: nightly - token: ${{ steps.nightly_token.outputs.token }} - tag: ${{ env.RELEASE_TAG }} - name: ${{ env.RELEASE_TAG }} - body: ${{ env.RELEASE_BODY }} - artifacts: mobile/native/android/apps/ensu/artifacts/${{ env.ANDROID_APK_NAME }} - prerelease: true - makeLatest: false - allowUpdates: true - updateOnlyUnreleased: true - - - name: Upload Android APK to RC draft - if: startsWith(github.ref_name, 'release/') - uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0 - with: - tag: ${{ env.RELEASE_TAG }} - name: ${{ env.RELEASE_TAG }} - body: ${{ env.RELEASE_BODY }} - commit: ${{ env.COMMIT_SHA }} - artifacts: mobile/native/android/apps/ensu/artifacts/${{ env.ANDROID_APK_NAME }} - draft: true - allowUpdates: true - updateOnlyUnreleased: true - - - name: Upload AAB to Play Store - uses: r0adkll/upload-google-play@e738b9dd8f2476ea806d921b64aacd24f34515a5 # v1.1.5 - with: - serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }} - packageName: io.ente.ensu - releaseFiles: mobile/native/android/apps/ensu/app/build/outputs/bundle/release/app-release.aab - tracks: internal - whatsNewDirectory: mobile/native/android/apps/ensu/whatsnew - - build-ios: - needs: build-metadata - if: needs.build-metadata.outputs.should_build == 'true' - runs-on: macos-26 - environment: - name: production - - defaults: - run: - working-directory: mobile/native/apple/apps/ensu - - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ needs.build-metadata.outputs.commit_sha }} - - - run: cargo codegen native - working-directory: rust - - - name: Import Apple Distribution certificate - uses: Apple-Actions/import-codesign-certs@5142e029c445c10ffc7149d172e540235a065466 # v7.0.0 - with: - p12-file-base64: ${{ secrets.APPLE_DISTRIBUTION_CERT_BASE64 }} - p12-password: ${{ secrets.APPLE_DISTRIBUTION_CERT_PASSWORD }} - - - name: Archive and upload Ensu to TestFlight - env: - ASC_API_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_API_KEY_BASE64 }} - ASC_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }} - ASC_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - BUILD_NUMBER: ${{ needs.build-metadata.outputs.build_number }} - run: | - key_path="${RUNNER_TEMP}/AuthKey_${ASC_KEY_ID}.p8" - printf '%s' "${ASC_API_KEY_BASE64}" | base64 --decode > "${key_path}" - - xcodebuild archive \ - -project Ensu.xcodeproj \ - -scheme Ensu \ - -configuration Release \ - -destination 'generic/platform=iOS' \ - -archivePath build/Ensu.xcarchive \ - -allowProvisioningUpdates \ - -authenticationKeyPath "${key_path}" \ - -authenticationKeyID "${ASC_KEY_ID}" \ - -authenticationKeyIssuerID "${ASC_ISSUER_ID}" \ - CODE_SIGN_STYLE=Manual \ - CODE_SIGN_IDENTITY="Apple Distribution" \ - DEVELOPMENT_TEAM="${APPLE_TEAM_ID}" \ - APP_STORE_PROFILE_SPECIFIER="Ensu App Store" \ - CURRENT_PROJECT_VERSION="${BUILD_NUMBER}" - - xcodebuild -exportArchive \ - -archivePath build/Ensu.xcarchive \ - -exportPath build/Export \ - -exportOptionsPlist scripts/ExportOptions-AppStore-CI.plist \ - -allowProvisioningUpdates \ - -authenticationKeyPath "${key_path}" \ - -authenticationKeyID "${ASC_KEY_ID}" \ - -authenticationKeyIssuerID "${ASC_ISSUER_ID}" - - finish-build: - needs: [build-metadata, build-desktop, build-android, build-ios] - if: needs.build-metadata.outputs.should_build == 'true' - runs-on: ubuntu-latest - - steps: - - name: Checkout built commit - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ needs.build-metadata.outputs.commit_sha }} - - - name: Point release tag at the successfully built commit - env: - COMMIT_SHA: ${{ needs.build-metadata.outputs.commit_sha }} - RELEASE_TAG: ${{ needs.build-metadata.outputs.release_tag }} - run: | - git tag --force "${RELEASE_TAG}" "${COMMIT_SHA}" - git push --force origin "refs/tags/${RELEASE_TAG}" - - - name: Notify Discord - if: github.event_name != 'schedule' || needs.build-metadata.outputs.has_new_changes == 'true' - continue-on-error: true - env: - DISCORD_WEBHOOK: ${{ secrets.DISCORD_INTERNAL_RELEASE_WEBHOOK }} - RELEASE_TITLE: ${{ needs.build-metadata.outputs.release_title }} - RELEASE_BODY_GROUPED: ${{ needs.build-metadata.outputs.release_body_grouped }} - RELEASE_TAG: ${{ needs.build-metadata.outputs.release_tag }} - run: node .github/scripts/release-notify.mjs ensu "${RELEASE_TAG}" diff --git a/.github/workflows/ensu-ios-build.yml b/.github/workflows/ensu-ios-build.yml deleted file mode 100644 index 1eb0df5356c..00000000000 --- a/.github/workflows/ensu-ios-build.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: "Build (Ensu iOS)" - -on: - pull_request: - paths: - - "mobile/native/apple/apps/ensu/**" - - "rust/apps/codegen/**" - - "rust/crates/core/**" - - "rust/crates/ensu/**" - - "rust/bindings/uniffi/**" - - "rust/.cargo/config.toml" - - "rust/Cargo.toml" - - "rust/Cargo.lock" - - ".github/workflows/ensu-ios-build.yml" - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - build-and-test: - runs-on: macos-26 - - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - # Restore-only; primed on main by warm-caches.yml. - - name: Restore Rust cache - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - with: - workspaces: rust - shared-key: ensu-ios - save-if: false - - - run: cargo codegen native - working-directory: rust - - # This pull_request workflow does not provide the inference fixtures, - # so this step is only a build + startup smoke test. - - name: Build and test Ensu - working-directory: mobile/native/apple/apps/ensu - run: xcodebuild test -scheme Ensu -destination 'platform=iOS Simulator,name=iPhone 17' diff --git a/.github/workflows/infra-deploy-staff.yml b/.github/workflows/infra-deploy-staff.yml deleted file mode 100644 index 87197fcbbd4..00000000000 --- a/.github/workflows/infra-deploy-staff.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: "Deploy (Staff)" - -on: - push: - branches: [main] - paths: - - ".github/workflows/infra-deploy-staff.yml" - - "infra/staff/**" - workflow_dispatch: - -permissions: - contents: read - -jobs: - deploy: - runs-on: ubuntu-latest - environment: production - - defaults: - run: - working-directory: infra/staff - - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - name: Setup node and enable npm caching - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: 24 - cache: "npm" - cache-dependency-path: "infra/staff/package-lock.json" - - - run: npm ci - - - run: npm run build - - - name: Publish - uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0 - with: - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - command: pages deploy --project-name=ente --commit-dirty=true --branch=deploy/staff infra/staff/dist diff --git a/.github/workflows/infra-lint-staff.yml b/.github/workflows/infra-lint-staff.yml deleted file mode 100644 index 4835d7a9ddb..00000000000 --- a/.github/workflows/infra-lint-staff.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: "Lint (Staff)" - -on: - pull_request: - paths: - - ".github/workflows/infra-lint-staff.yml" - - "infra/staff/**" - -permissions: - contents: read - -jobs: - lint: - runs-on: ubuntu-latest - - defaults: - run: - working-directory: infra/staff - - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - name: Setup node and enable npm caching - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: 24 - cache: "npm" - cache-dependency-path: "infra/staff/package-lock.json" - - - name: Install dependencies - run: npm ci - - - name: Lint - run: npm run lint diff --git a/.github/workflows/locker-build.yml b/.github/workflows/locker-build.yml deleted file mode 100644 index 5b525ede2be..00000000000 --- a/.github/workflows/locker-build.yml +++ /dev/null @@ -1,355 +0,0 @@ -name: "Build (Locker)" - -# See .github/docs/app-release.md - -on: - push: - branches: - - "release/locker-v*" - workflow_dispatch: - schedule: - # Run every weekday at ~4:45 AM IST - - cron: "15 23 * * 0-4" - -permissions: - contents: write - -concurrency: - group: locker-build - cancel-in-progress: false - -jobs: - build-metadata: - runs-on: ubuntu-latest - - outputs: - should_build: ${{ steps.prepare.outputs.should_build }} - commit_sha: ${{ steps.prepare.outputs.commit_sha }} - build_number: ${{ steps.prepare.outputs.build_number }} - release_version: ${{ steps.prepare.outputs.release_version }} - release_tag: ${{ steps.prepare.outputs.release_tag }} - release_title: ${{ steps.prepare.outputs.release_title }} - release_body: ${{ steps.prepare.outputs.release_body }} - release_body_grouped: ${{ steps.prepare.outputs.release_body_grouped }} - has_new_changes: ${{ steps.prepare.outputs.has_new_changes }} - play_store_release_notes: ${{ steps.prepare.outputs.play_store_release_notes }} - artifact_stem: ${{ steps.prepare.outputs.artifact_stem }} - - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ github.ref_name }} - - - name: Prepare release - id: prepare - shell: bash - run: | - set -euo pipefail - - if [[ "${GITHUB_EVENT_NAME}" == "schedule" ]] && git ls-remote --exit-code --heads origin 'release/locker-v*' >/dev/null 2>&1; then - echo "Active Locker release branch found; skipping scheduled nightly" - echo "should_build=false" >> "${GITHUB_OUTPUT}" - exit 0 - fi - - release_version="$(node .github/scripts/flutter-version.mjs locker get)" - - if [[ "${GITHUB_REF_NAME}" == release/locker-v* ]]; then - if [[ "${GITHUB_REF_NAME}" != "release/locker-v${release_version}" ]]; then - echo "::error::Branch ${GITHUB_REF_NAME} does not match Locker version ${release_version}" - exit 1 - fi - build_number="$(node .github/scripts/flutter-version.mjs locker get-build-base)" - release_tag="locker-v${release_version}-rc" - release_title="๐Ÿ—„๏ธ Locker release candidate - ${release_version} (build ${build_number})" - else - build_number_base="$(node .github/scripts/flutter-version.mjs locker get-build-base)" - build_number=$((build_number_base + GITHUB_RUN_NUMBER)) - release_tag="locker-v${release_version}-beta" - release_title="๐Ÿ—„๏ธ Locker nightly - ${release_version} (build ${build_number})" - fi - - commit_sha="$(git rev-parse HEAD)" - git fetch --force --depth=1 --no-tags origin "refs/tags/${release_tag}:refs/tags/${release_tag}" || true - - echo "should_build=true" >> "${GITHUB_OUTPUT}" - echo "commit_sha=${commit_sha}" >> "${GITHUB_OUTPUT}" - echo "build_number=${build_number}" >> "${GITHUB_OUTPUT}" - echo "release_version=${release_version}" >> "${GITHUB_OUTPUT}" - echo "release_tag=${release_tag}" >> "${GITHUB_OUTPUT}" - echo "release_title=${release_title}" >> "${GITHUB_OUTPUT}" - echo "artifact_stem=ente-${release_tag%-rc}" >> "${GITHUB_OUTPUT}" - node .github/scripts/release-notes.mjs mobile/apps/locker/changes "${release_tag}" >> "${GITHUB_OUTPUT}" - - build-android: - needs: build-metadata - if: needs.build-metadata.outputs.should_build == 'true' - runs-on: ubuntu-latest - environment: - name: production - env: - COMMIT_SHA: ${{ needs.build-metadata.outputs.commit_sha }} - BUILD_NUMBER: ${{ needs.build-metadata.outputs.build_number }} - RELEASE_VERSION: ${{ needs.build-metadata.outputs.release_version }} - RELEASE_TAG: ${{ needs.build-metadata.outputs.release_tag }} - RELEASE_BODY: ${{ needs.build-metadata.outputs.release_body }} - PLAY_STORE_RELEASE_NOTES: ${{ needs.build-metadata.outputs.play_store_release_notes }} - ARTIFACT_STEM: ${{ needs.build-metadata.outputs.artifact_stem }} - - defaults: - run: - working-directory: mobile/apps/locker - - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ needs.build-metadata.outputs.commit_sha }} - - - name: Setup JDK 17 - uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 - with: - distribution: "temurin" - java-version: "17" - - - name: Install Flutter - uses: ./.github/actions/setup-flutter - - - name: Set build version - run: node ../../../.github/scripts/flutter-version.mjs locker set-build "${RELEASE_VERSION}" "${BUILD_NUMBER}" - - - run: flutter pub get --enforce-lockfile - - - run: cargo codegen frb shared - working-directory: rust - - - name: Prepare whatsnew - run: | - mkdir -p whatsnew - printf '%s' "${PLAY_STORE_RELEASE_NOTES}" > whatsnew/whatsnew-en-US - - - name: Add keystore - run: printf '%s' "${SIGNING_KEY_LOCKER}" | base64 -d > "$RUNNER_TEMP/ente_locker_key.jks" - env: - SIGNING_KEY_LOCKER: ${{ secrets.SIGNING_KEY_LOCKER }} - - - name: Create artifacts directory - run: mkdir artifacts - - - name: Build independent APK - run: | - flutter build apk --build-name="${RELEASE_VERSION}" --build-number="${BUILD_NUMBER}" --dart-define=cronetHttpNoPlay=true --release --flavor independent - mv build/app/outputs/flutter-apk/app-independent-release.apk "artifacts/${ARTIFACT_STEM}.apk" - env: - SIGNING_KEY_PATH: ${{ runner.temp }}/ente_locker_key.jks - SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS_LOCKER }} - SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD_LOCKER }} - SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD_LOCKER }} - - - name: Build PlayStore AAB - run: flutter build appbundle --build-name="${RELEASE_VERSION}" --build-number="${BUILD_NUMBER}" --dart-define=cronetHttpNoPlay=true --release --flavor playstore - env: - SIGNING_KEY_PATH: ${{ runner.temp }}/ente_locker_key.jks - SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS_LOCKER }} - SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD_LOCKER }} - SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD_LOCKER }} - - - name: Generate checksums - run: cd artifacts && sha256sum ente-locker-*.apk > SHA256SUMS-android - - - name: Upload checksum snippet - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: locker-SHA256SUMS-android - path: mobile/apps/locker/artifacts/SHA256SUMS-android - - - name: Mint nightly token - id: nightly_token - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 - with: - client-id: ${{ secrets.ENTE_CI_GH_CLIENT_ID }} - private-key: ${{ secrets.ENTE_CI_GH_APP_PRIVATE_KEY }} - repositories: nightly - permission-contents: write - - - name: Upload Android artifact to nightly release - uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0 - with: - repo: nightly - token: ${{ steps.nightly_token.outputs.token }} - tag: ${{ env.RELEASE_TAG }} - name: ${{ env.RELEASE_TAG }} - body: ${{ env.RELEASE_BODY }} - artifacts: "mobile/apps/locker/artifacts/ente-locker-*.apk" - prerelease: true - makeLatest: false - allowUpdates: true - updateOnlyUnreleased: true - - - name: Upload Android artifact to RC draft - if: startsWith(github.ref_name, 'release/') - uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0 - with: - tag: ${{ env.RELEASE_TAG }} - name: ${{ env.RELEASE_TAG }} - body: ${{ env.RELEASE_BODY }} - commit: ${{ env.COMMIT_SHA }} - artifacts: "mobile/apps/locker/artifacts/ente-locker-*.apk" - draft: true - allowUpdates: true - updateOnlyUnreleased: true - - - name: Upload AAB to PlayStore - uses: r0adkll/upload-google-play@e738b9dd8f2476ea806d921b64aacd24f34515a5 # v1.1.5 - with: - serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }} - packageName: io.ente.locker - releaseFiles: mobile/apps/locker/build/app/outputs/bundle/playstoreRelease/app-playstore-release.aab - tracks: internal - whatsNewDirectory: mobile/apps/locker/whatsnew - mappingFile: mobile/apps/locker/build/app/outputs/mapping/playstoreRelease/mapping.txt - - build-ios: - needs: build-metadata - if: needs.build-metadata.outputs.should_build == 'true' - runs-on: macos-26 - environment: - name: production - env: - BUILD_NUMBER: ${{ needs.build-metadata.outputs.build_number }} - RELEASE_VERSION: ${{ needs.build-metadata.outputs.release_version }} - - defaults: - run: - working-directory: mobile/apps/locker - - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ needs.build-metadata.outputs.commit_sha }} - - - name: Install Flutter - uses: ./.github/actions/setup-flutter - - - name: Set build version - run: node ../../../.github/scripts/flutter-version.mjs locker set-build "${RELEASE_VERSION}" "${BUILD_NUMBER}" - - - run: flutter pub get --enforce-lockfile - - - name: Set up Rive iOS binaries - run: dart run rive_native:setup --clean --platform ios - - - run: cargo codegen frb shared - working-directory: rust - - - name: Install iOS pods - run: pod install --deployment - working-directory: mobile/apps/locker/ios - - - name: Import Apple Distribution certificate - uses: Apple-Actions/import-codesign-certs@5142e029c445c10ffc7149d172e540235a065466 # v7.0.0 - with: - p12-file-base64: ${{ secrets.APPLE_DISTRIBUTION_CERT_BASE64 }} - p12-password: ${{ secrets.APPLE_DISTRIBUTION_CERT_PASSWORD }} - - - name: Archive and upload Locker to TestFlight - env: - ASC_API_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_API_KEY_BASE64 }} - ASC_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }} - ASC_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - run: | - key_path="${RUNNER_TEMP}/AuthKey_${ASC_KEY_ID}.p8" - printf '%s' "${ASC_API_KEY_BASE64}" | base64 --decode > "${key_path}" - - xcodebuild archive \ - -workspace ios/Runner.xcworkspace \ - -scheme Runner \ - -configuration Release \ - -destination 'generic/platform=iOS' \ - -archivePath build/ios/archive/Runner.xcarchive \ - -allowProvisioningUpdates \ - -authenticationKeyPath "${key_path}" \ - -authenticationKeyID "${ASC_KEY_ID}" \ - -authenticationKeyIssuerID "${ASC_ISSUER_ID}" \ - CODE_SIGN_STYLE=Manual \ - CODE_SIGN_IDENTITY="Apple Distribution" \ - DEVELOPMENT_TEAM="${APPLE_TEAM_ID}" \ - APP_STORE_PROFILE_SPECIFIER="Locker App Store" \ - SHARE_EXTENSION_APP_STORE_PROFILE_SPECIFIER="Locker Share Extension App Store" \ - MARKETING_VERSION="${RELEASE_VERSION}" \ - CURRENT_PROJECT_VERSION="${BUILD_NUMBER}" - - xcodebuild -exportArchive \ - -archivePath build/ios/archive/Runner.xcarchive \ - -exportPath build/ios/export \ - -exportOptionsPlist scripts/ExportOptions-AppStore-CI.plist \ - -allowProvisioningUpdates \ - -authenticationKeyPath "${key_path}" \ - -authenticationKeyID "${ASC_KEY_ID}" \ - -authenticationKeyIssuerID "${ASC_ISSUER_ID}" - - finish-build: - needs: [build-metadata, build-android, build-ios] - if: needs.build-metadata.outputs.should_build == 'true' - runs-on: ubuntu-latest - environment: - name: production - - steps: - - name: Checkout built commit - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ needs.build-metadata.outputs.commit_sha }} - - - name: Download checksum snippets - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - pattern: locker-SHA256SUMS-* - path: checksums - merge-multiple: true - - - name: Combine checksums - run: sort -k2 checksums/SHA256SUMS-* > SHA256SUMS - - - name: Point release tag at the successfully built commit - env: - COMMIT_SHA: ${{ needs.build-metadata.outputs.commit_sha }} - RELEASE_TAG: ${{ needs.build-metadata.outputs.release_tag }} - run: | - git tag --force "${RELEASE_TAG}" "${COMMIT_SHA}" - git push --force origin "refs/tags/${RELEASE_TAG}" - - - name: Mint nightly token - id: nightly_token - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 - with: - client-id: ${{ secrets.ENTE_CI_GH_CLIENT_ID }} - private-key: ${{ secrets.ENTE_CI_GH_APP_PRIVATE_KEY }} - repositories: nightly - permission-contents: write - - - name: Upload checksum to nightly release - env: - GH_TOKEN: ${{ steps.nightly_token.outputs.token }} - RELEASE_TAG: ${{ needs.build-metadata.outputs.release_tag }} - run: gh release upload "${RELEASE_TAG}" SHA256SUMS --clobber --repo ente/nightly - - - name: Upload checksum to RC draft - if: startsWith(github.ref_name, 'release/') - env: - GH_TOKEN: ${{ github.token }} - RELEASE_TAG: ${{ needs.build-metadata.outputs.release_tag }} - run: gh release upload "${RELEASE_TAG}" SHA256SUMS --clobber - - - name: Notify Discord - if: github.event_name != 'schedule' || needs.build-metadata.outputs.has_new_changes == 'true' - continue-on-error: true - env: - DISCORD_WEBHOOK: ${{ secrets.DISCORD_INTERNAL_RELEASE_WEBHOOK }} - RELEASE_TITLE: ${{ needs.build-metadata.outputs.release_title }} - RELEASE_BODY_GROUPED: ${{ needs.build-metadata.outputs.release_body_grouped }} - RELEASE_TAG: ${{ needs.build-metadata.outputs.release_tag }} - run: node .github/scripts/release-notify.mjs locker "${RELEASE_TAG}" diff --git a/.github/workflows/mobile-crowdin-push-sources-and-translations.yml b/.github/workflows/mobile-crowdin-push-sources-and-translations.yml deleted file mode 100644 index a6179b54db5..00000000000 --- a/.github/workflows/mobile-crowdin-push-sources-and-translations.yml +++ /dev/null @@ -1,85 +0,0 @@ -name: "Push sources and translations to Crowdin (Mobile)" - -# This is a variant of the mobile Crowdin sync workflows that uploads the -# translated strings in addition to the source strings. -# -# This allows us to change the strings in our source code for an automated -# refactoring (e.g. renaming a placeholder), and then run this workflow to -# update the data in Crowdin taking our source code as the source of truth. - -# USAGE: -# -# Be careful with this workflow since it updates both the sources and -# translations on Crowdin, and any changes on Crowdin that are not in the -# branch from which this workflow is run will be lost. -# -# 1. Run the normal sync workflow: -# `gh workflow run mobile-crowdin-sync.yml` -# -# 2. Merge the translations PR it creates. So now `main` includes the latest -# Crowdin translations. -# -# 3. Branch from updated `main` and make the string edits. -# -# 4. Push the branch and run this workflow -# `gh workflow run mobile-crowdin-push-sources-and-translations.yml --ref -f app=photos` -# -# 5. After it succeeds, merge the string-edit PR. - -on: - # Trigger manually, or using - # `gh workflow run mobile-crowdin-push-sources-and-translations.yml --ref -f app=photos` - workflow_dispatch: - inputs: - app: - description: "Mobile app to push" - required: true - default: "all" - type: choice - options: - - all - - photos - - auth - - locker - -permissions: - contents: read - -jobs: - push-sources-and-translations-to-crowdin: - name: "Push ${{ matrix.app }} sources and translations" - runs-on: ubuntu-latest - strategy: - matrix: - include: - - app: photos - base_path: "mobile/apps/photos/" - config: "mobile/apps/photos/crowdin.yml" - project_id: 574741 - - app: auth - base_path: "mobile/apps/auth/" - config: "mobile/apps/auth/crowdin.yml" - project_id: 575169 - - app: locker - base_path: "mobile/apps/locker/" - config: "mobile/apps/locker/crowdin.yml" - project_id: 860056 - - steps: - - name: Checkout - if: ${{ inputs.app == 'all' || inputs.app == matrix.app }} - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - name: Crowdin push - if: ${{ inputs.app == 'all' || inputs.app == matrix.app }} - uses: crowdin/github-action@52aa776766211d83d975df51f3b9c53c2f8ba35f # v2.16.3 - with: - base_path: ${{ matrix.base_path }} - config: ${{ matrix.config }} - upload_sources: true - upload_translations: true - download_translations: false - project_id: ${{ matrix.project_id }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} diff --git a/.github/workflows/mobile-crowdin-push-sources.yml b/.github/workflows/mobile-crowdin-push-sources.yml deleted file mode 100644 index 8fb8c69922e..00000000000 --- a/.github/workflows/mobile-crowdin-push-sources.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: "Push sources to Crowdin (Mobile)" - -on: - push: - branches: [main] - paths: - - ".github/workflows/mobile-crowdin-push-sources.yml" - - "mobile/apps/photos/lib/l10n/intl_en.arb" - - "mobile/apps/photos/crowdin.yml" - - "mobile/apps/auth/lib/l10n/arb/app_en.arb" - - "mobile/apps/auth/crowdin.yml" - - "mobile/apps/locker/lib/l10n/app_en.arb" - - "mobile/apps/locker/crowdin.yml" - -concurrency: - group: ${{ github.workflow }} - cancel-in-progress: false - -permissions: - contents: read - -jobs: - push-sources-to-crowdin: - name: "Push ${{ matrix.app }} sources" - runs-on: ubuntu-latest - strategy: - matrix: - include: - - app: photos - base_path: "mobile/apps/photos/" - config: "mobile/apps/photos/crowdin.yml" - project_id: 574741 - - app: auth - base_path: "mobile/apps/auth/" - config: "mobile/apps/auth/crowdin.yml" - project_id: 575169 - - app: locker - base_path: "mobile/apps/locker/" - config: "mobile/apps/locker/crowdin.yml" - project_id: 860056 - - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - name: Crowdin push - uses: crowdin/github-action@52aa776766211d83d975df51f3b9c53c2f8ba35f # v2.16.3 - with: - base_path: ${{ matrix.base_path }} - config: ${{ matrix.config }} - upload_sources: true - upload_translations: false - download_translations: false - project_id: ${{ matrix.project_id }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} diff --git a/.github/workflows/mobile-crowdin-sync.yml b/.github/workflows/mobile-crowdin-sync.yml deleted file mode 100644 index 53fdfccd10d..00000000000 --- a/.github/workflows/mobile-crowdin-sync.yml +++ /dev/null @@ -1,78 +0,0 @@ -name: "Sync Crowdin (Mobile)" - -on: - schedule: - # Run Mondays at ~6:20 AM IST - # See: [Note: Run workflow on specific days of the week] - - cron: "50 0 * * 1" - # Also allow manually running the workflow. - workflow_dispatch: - inputs: - app: - description: "Mobile app to sync" - required: true - default: "all" - type: choice - options: - - all - - photos - - auth - - locker - -permissions: - contents: write - pull-requests: write - -jobs: - synchronize-with-crowdin: - name: "Sync ${{ matrix.app }} with Crowdin" - runs-on: ubuntu-latest - strategy: - matrix: - include: - - app: photos - base_path: "mobile/apps/photos/" - config: "mobile/apps/photos/crowdin.yml" - project_id: 574741 - localization_branch_name: translations/photos - pull_request_title: "[mobile/photos] New translations" - pull_request_body: "New translations from [Crowdin](https://crowdin.com/project/ente-photos-app)" - - app: auth - base_path: "mobile/apps/auth/" - config: "mobile/apps/auth/crowdin.yml" - project_id: 575169 - localization_branch_name: translations/auth - pull_request_title: "[mobile/auth] New translations" - pull_request_body: "New translations from [Crowdin](https://crowdin.com/project/ente-authenticator-app)" - - app: locker - base_path: "mobile/apps/locker/" - config: "mobile/apps/locker/crowdin.yml" - project_id: 860056 - localization_branch_name: translations/locker - pull_request_title: "[mobile/locker] New translations" - pull_request_body: "New translations from [Crowdin](https://crowdin.com/project/ente-locker)" - - steps: - - name: Checkout - if: ${{ github.event_name != 'workflow_dispatch' || inputs.app == 'all' || inputs.app == matrix.app }} - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - name: Crowdin's action - if: ${{ github.event_name != 'workflow_dispatch' || inputs.app == 'all' || inputs.app == matrix.app }} - uses: crowdin/github-action@52aa776766211d83d975df51f3b9c53c2f8ba35f # v2.16.3 - with: - base_path: ${{ matrix.base_path }} - config: ${{ matrix.config }} - upload_sources: true - upload_translations: false - download_translations: true - localization_branch_name: ${{ matrix.localization_branch_name }} - create_pull_request: true - skip_untranslated_strings: true - pull_request_title: ${{ matrix.pull_request_title }} - pull_request_body: ${{ matrix.pull_request_body }} - pull_request_base_branch_name: "main" - project_id: ${{ matrix.project_id }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} diff --git a/.github/workflows/mobile-lint.yml b/.github/workflows/mobile-lint.yml deleted file mode 100644 index 9ba9bb6aee5..00000000000 --- a/.github/workflows/mobile-lint.yml +++ /dev/null @@ -1,116 +0,0 @@ -name: "Lint (Mobile)" - -on: - pull_request: - paths: - - ".github/workflows/mobile-lint.yml" - - "mobile/**" - - "!mobile/native/**" - - "rust/apps/codegen/**" - - "rust/bindings/frb/**" - - "rust/Cargo.lock" - - "rust/Cargo.toml" - - "rust/crates/contacts/**" - - "rust/crates/core/**" - - "rust/crates/image/**" - - "rust/crates/photos/**" - workflow_dispatch: - -permissions: - contents: read - -env: - RUSTFLAGS: -D warnings - -jobs: - lint: - runs-on: ubuntu-latest - steps: - - name: Checkout code and submodules - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - submodules: recursive - - - name: Verify frozen mobile pubspecs - run: ruby mobile/scripts/check_frozen_pubspecs.rb mobile - - - name: Verify source ARB plurals - run: ruby mobile/scripts/check_source_arb_plurals.rb - - - name: Verify auth custom icons - run: | - find assets/custom-icons -type f -name "*.svg" | while read -r file; do - name=$(basename "$file") - if [[ "$name" != "$(printf "%s" "$name" | tr '[:upper:]' '[:lower:]' | tr ' ' '_')" ]]; then - echo "File name is not lowercase: $file" - exit 1 - fi - - if [[ "$file" == "assets/custom-icons/icons/bbs_nga.svg" ]]; then - continue - fi - - size=$(wc -c < "$file") - if (( size > 20480 )); then - echo "File size is greater than 20KB: $file ($size bytes)" - exit 1 - fi - done - - jq empty assets/custom-icons/_data/custom-icons.json - jq -r '.icons[] | select(.hex != null) | .hex' assets/custom-icons/_data/custom-icons.json | while read -r hex; do - if [[ -n "$hex" && ! "$hex" =~ ^[0-9a-fA-F]{6}$ ]]; then - echo "Invalid hex color '$hex'. Must be exactly 6 hexadecimal characters." - exit 1 - fi - done - working-directory: mobile/apps/auth - - - name: Install Flutter - uses: ./.github/actions/setup-flutter - - # Restore-only; primed on main by warm-caches.yml. - - name: Restore Rust cache - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - with: - workspaces: rust - shared-key: rust-flutter - save-if: false - - - run: cargo codegen frb - working-directory: rust - - - run: cargo clippy -p ente_photos_rust --features flutter --all-targets --locked - working-directory: rust - - - run: cargo clippy -p ente_rust --features flutter --all-targets --locked - working-directory: rust - - - run: flutter pub get --enforce-lockfile - working-directory: mobile - - - run: dart format --output=none --set-exit-if-changed . - working-directory: mobile - - - run: flutter analyze --no-pub - working-directory: mobile - - - name: Test - working-directory: mobile - run: | - test_package() ( - member=${1#"$PWD/"} - echo "Testing $member" - cd "$member" - flutter test --no-pub - ) - export -f test_package - - dart pub workspace list --json | - jq -r '.packages[] | select(.name != "ente_workspace") | .path' | - while IFS= read -r member; do - if [[ -d "$member/test" ]]; then - printf '%s\n' "$member" - fi - done | - xargs -P4 -n1 bash -e -c 'test_package "$1"' _ diff --git a/.github/workflows/mobile-podfile-lock.yml b/.github/workflows/mobile-podfile-lock.yml deleted file mode 100644 index 5d875cce502..00000000000 --- a/.github/workflows/mobile-podfile-lock.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: "Verify Podfile.lock (Mobile)" - -# Verify each app's Podfile.lock is in sync with its Flutter plugins. - -on: - pull_request: - paths: - - ".github/workflows/mobile-podfile-lock.yml" - - "mobile/**/pubspec.yaml" - - "mobile/**/pubspec.lock" - - "mobile/**/Podfile" - - "mobile/**/Podfile.lock" - - "mobile/**/*.podspec" - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - pod-check: - runs-on: macos-26 - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - name: Install Flutter - uses: ./.github/actions/setup-flutter - - - run: flutter pub get --enforce-lockfile - working-directory: mobile - - - name: Verify Podfile.lock is current - run: | - for app in photos auth locker; do - echo "Checking $app" - ( cd "mobile/apps/$app/ios" && pod install --deployment ) - done diff --git a/.github/workflows/photos-build.yml b/.github/workflows/photos-build.yml deleted file mode 100644 index a3a589f4d70..00000000000 --- a/.github/workflows/photos-build.yml +++ /dev/null @@ -1,361 +0,0 @@ -name: "Build (Photos)" - -# See .github/docs/app-release.md - -on: - push: - branches: - - "release/photos-v*" - workflow_dispatch: - schedule: - # Run every weekday at ~2:35 AM IST - - cron: "5 21 * * 0-4" - -permissions: - contents: write - -concurrency: - group: photos-build - cancel-in-progress: false - -jobs: - build-metadata: - runs-on: ubuntu-latest - - outputs: - should_build: ${{ steps.prepare.outputs.should_build }} - commit_sha: ${{ steps.prepare.outputs.commit_sha }} - build_number: ${{ steps.prepare.outputs.build_number }} - release_version: ${{ steps.prepare.outputs.release_version }} - release_tag: ${{ steps.prepare.outputs.release_tag }} - release_title: ${{ steps.prepare.outputs.release_title }} - release_body: ${{ steps.prepare.outputs.release_body }} - release_body_grouped: ${{ steps.prepare.outputs.release_body_grouped }} - has_new_changes: ${{ steps.prepare.outputs.has_new_changes }} - play_store_release_notes: ${{ steps.prepare.outputs.play_store_release_notes }} - artifact_stem: ${{ steps.prepare.outputs.artifact_stem }} - - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ github.ref_name }} - - - name: Prepare release - id: prepare - shell: bash - run: | - set -euo pipefail - - if [[ "${GITHUB_EVENT_NAME}" == "schedule" ]] && git ls-remote --exit-code --heads origin 'release/photos-v*' >/dev/null 2>&1; then - echo "Active Photos release branch found; skipping scheduled nightly" - echo "should_build=false" >> "${GITHUB_OUTPUT}" - exit 0 - fi - - release_version="$(node .github/scripts/flutter-version.mjs photos get)" - - if [[ "${GITHUB_REF_NAME}" == release/photos-v* ]]; then - if [[ "${GITHUB_REF_NAME}" != "release/photos-v${release_version}" ]]; then - echo "::error::Branch ${GITHUB_REF_NAME} does not match Photos version ${release_version}" - exit 1 - fi - build_number="$(node .github/scripts/flutter-version.mjs photos get-build-base)" - release_tag="photos-v${release_version}-rc" - release_title="๐ŸŒป Photos release candidate - ${release_version} (build ${build_number})" - else - build_number_base="$(node .github/scripts/flutter-version.mjs photos get-build-base)" - build_number=$((build_number_base + GITHUB_RUN_NUMBER)) - release_tag="photos-v${release_version}-beta" - release_title="๐ŸŒป Photos nightly - ${release_version} (build ${build_number})" - fi - - commit_sha="$(git rev-parse HEAD)" - git fetch --force --depth=1 --no-tags origin "refs/tags/${release_tag}:refs/tags/${release_tag}" || true - - echo "should_build=true" >> "${GITHUB_OUTPUT}" - echo "commit_sha=${commit_sha}" >> "${GITHUB_OUTPUT}" - echo "build_number=${build_number}" >> "${GITHUB_OUTPUT}" - echo "release_version=${release_version}" >> "${GITHUB_OUTPUT}" - echo "release_tag=${release_tag}" >> "${GITHUB_OUTPUT}" - echo "release_title=${release_title}" >> "${GITHUB_OUTPUT}" - echo "artifact_stem=ente-${release_tag%-rc}" >> "${GITHUB_OUTPUT}" - node .github/scripts/release-notes.mjs mobile/apps/photos/changes "${release_tag}" >> "${GITHUB_OUTPUT}" - - build-android: - needs: build-metadata - if: needs.build-metadata.outputs.should_build == 'true' - runs-on: ubuntu-latest - environment: - name: production - env: - COMMIT_SHA: ${{ needs.build-metadata.outputs.commit_sha }} - BUILD_NUMBER: ${{ needs.build-metadata.outputs.build_number }} - RELEASE_VERSION: ${{ needs.build-metadata.outputs.release_version }} - RELEASE_TAG: ${{ needs.build-metadata.outputs.release_tag }} - RELEASE_BODY: ${{ needs.build-metadata.outputs.release_body }} - PLAY_STORE_RELEASE_NOTES: ${{ needs.build-metadata.outputs.play_store_release_notes }} - ARTIFACT_STEM: ${{ needs.build-metadata.outputs.artifact_stem }} - - defaults: - run: - working-directory: mobile/apps/photos - - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ needs.build-metadata.outputs.commit_sha }} - - - name: Setup JDK 17 - uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 - with: - distribution: "temurin" - java-version: "17" - - - name: Install Flutter - uses: ./.github/actions/setup-flutter - - - name: Set build version - run: node ../../../.github/scripts/flutter-version.mjs photos set-build "${RELEASE_VERSION}" "${BUILD_NUMBER}" - - - run: flutter pub get --enforce-lockfile - - - run: cargo codegen frb - working-directory: rust - - - name: Free runner disk space - run: sudo rm -rf /usr/share/dotnet - - - name: Prepare whatsnew - run: | - mkdir -p whatsnew - printf '%s' "${PLAY_STORE_RELEASE_NOTES}" > whatsnew/whatsnew-en-US - - - name: Add keystore - run: printf '%s' "${SIGNING_KEY_PHOTOS}" | base64 -d > "$RUNNER_TEMP/ente_photos_key.jks" - env: - SIGNING_KEY_PHOTOS: ${{ secrets.SIGNING_KEY_PHOTOS }} - - - name: Create artifacts directory - run: mkdir artifacts - - - name: Build independent APK - run: | - flutter build apk --build-name="${RELEASE_VERSION}" --build-number="${BUILD_NUMBER}" --dart-define=cronetHttpNoPlay=true --release --flavor independent --target-platform android-arm,android-arm64 - mv build/app/outputs/flutter-apk/app-independent-release.apk "artifacts/${ARTIFACT_STEM}.apk" - env: - SIGNING_KEY_PATH: ${{ runner.temp }}/ente_photos_key.jks - SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS_PHOTOS }} - SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD_PHOTOS }} - SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD_PHOTOS }} - - - name: Build PlayStore AAB - run: flutter build appbundle --build-name="${RELEASE_VERSION}" --build-number="${BUILD_NUMBER}" --dart-define=cronetHttpNoPlay=true --release --flavor playstore --target-platform android-arm,android-arm64 - env: - SIGNING_KEY_PATH: ${{ runner.temp }}/ente_photos_key.jks - SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS_PHOTOS }} - SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD_PHOTOS }} - SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD_PHOTOS }} - - - name: Generate checksums - run: cd artifacts && sha256sum ente-photos-*.apk > SHA256SUMS-android - - - name: Upload checksum snippet - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: photos-SHA256SUMS-android - path: mobile/apps/photos/artifacts/SHA256SUMS-android - - - name: Mint nightly token - id: nightly_token - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 - with: - client-id: ${{ secrets.ENTE_CI_GH_CLIENT_ID }} - private-key: ${{ secrets.ENTE_CI_GH_APP_PRIVATE_KEY }} - repositories: nightly - permission-contents: write - - - name: Upload Android artifact to nightly release - uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0 - with: - repo: nightly - token: ${{ steps.nightly_token.outputs.token }} - tag: ${{ env.RELEASE_TAG }} - name: ${{ env.RELEASE_TAG }} - body: ${{ env.RELEASE_BODY }} - artifacts: "mobile/apps/photos/artifacts/ente-photos-*.apk" - prerelease: true - makeLatest: false - allowUpdates: true - updateOnlyUnreleased: true - - - name: Upload Android artifact to RC draft - if: startsWith(github.ref_name, 'release/') - uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0 - with: - tag: ${{ env.RELEASE_TAG }} - name: ${{ env.RELEASE_TAG }} - body: ${{ env.RELEASE_BODY }} - commit: ${{ env.COMMIT_SHA }} - artifacts: "mobile/apps/photos/artifacts/ente-photos-*.apk" - draft: true - allowUpdates: true - updateOnlyUnreleased: true - - - name: Upload AAB to PlayStore - uses: r0adkll/upload-google-play@e738b9dd8f2476ea806d921b64aacd24f34515a5 # v1.1.5 - with: - serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }} - packageName: io.ente.photos - releaseFiles: mobile/apps/photos/build/app/outputs/bundle/playstoreRelease/app-playstore-release.aab - tracks: internal - whatsNewDirectory: mobile/apps/photos/whatsnew - mappingFile: mobile/apps/photos/build/app/outputs/mapping/playstoreRelease/mapping.txt - - build-ios: - needs: build-metadata - if: needs.build-metadata.outputs.should_build == 'true' - runs-on: macos-26 - environment: - name: production - env: - BUILD_NUMBER: ${{ needs.build-metadata.outputs.build_number }} - RELEASE_VERSION: ${{ needs.build-metadata.outputs.release_version }} - - defaults: - run: - working-directory: mobile/apps/photos - - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ needs.build-metadata.outputs.commit_sha }} - - - name: Install Flutter - uses: ./.github/actions/setup-flutter - - - name: Set build version - run: node ../../../.github/scripts/flutter-version.mjs photos set-build "${RELEASE_VERSION}" "${BUILD_NUMBER}" - - - run: flutter pub get --enforce-lockfile - - - name: Set up Rive iOS binaries - run: dart run rive_native:setup --clean --platform ios - - - run: cargo codegen frb - working-directory: rust - - - name: Install iOS pods - run: pod install --deployment - working-directory: mobile/apps/photos/ios - - - name: Import Apple Distribution certificate - uses: Apple-Actions/import-codesign-certs@5142e029c445c10ffc7149d172e540235a065466 # v7.0.0 - with: - p12-file-base64: ${{ secrets.APPLE_DISTRIBUTION_CERT_BASE64 }} - p12-password: ${{ secrets.APPLE_DISTRIBUTION_CERT_PASSWORD }} - - - name: Archive and upload Photos to TestFlight - env: - ASC_API_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_API_KEY_BASE64 }} - ASC_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }} - ASC_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - run: | - key_path="${RUNNER_TEMP}/AuthKey_${ASC_KEY_ID}.p8" - printf '%s' "${ASC_API_KEY_BASE64}" | base64 --decode > "${key_path}" - - xcodebuild archive \ - -workspace ios/Runner.xcworkspace \ - -scheme Runner \ - -configuration Release \ - -destination 'generic/platform=iOS' \ - -archivePath build/ios/archive/Runner.xcarchive \ - -allowProvisioningUpdates \ - -authenticationKeyPath "${key_path}" \ - -authenticationKeyID "${ASC_KEY_ID}" \ - -authenticationKeyIssuerID "${ASC_ISSUER_ID}" \ - CODE_SIGN_STYLE=Manual \ - CODE_SIGN_IDENTITY="Apple Distribution" \ - DEVELOPMENT_TEAM="${APPLE_TEAM_ID}" \ - APP_STORE_PROFILE_SPECIFIER="Photos App Store" \ - SHARE_EXTENSION_APP_STORE_PROFILE_SPECIFIER="Photos Share Extension App Store" \ - ALBUM_WIDGET_APP_STORE_PROFILE_SPECIFIER="Photos Album Widget App Store" \ - MEMORY_WIDGET_APP_STORE_PROFILE_SPECIFIER="Photos Memory Widget App Store" \ - PEOPLE_WIDGET_APP_STORE_PROFILE_SPECIFIER="Photos People Widget App Store" \ - MARKETING_VERSION="${RELEASE_VERSION}" \ - CURRENT_PROJECT_VERSION="${BUILD_NUMBER}" - - xcodebuild -exportArchive \ - -archivePath build/ios/archive/Runner.xcarchive \ - -exportPath build/ios/export \ - -exportOptionsPlist scripts/ExportOptions-AppStore-CI.plist \ - -allowProvisioningUpdates \ - -authenticationKeyPath "${key_path}" \ - -authenticationKeyID "${ASC_KEY_ID}" \ - -authenticationKeyIssuerID "${ASC_ISSUER_ID}" - - finish-build: - needs: [build-metadata, build-android, build-ios] - if: needs.build-metadata.outputs.should_build == 'true' - runs-on: ubuntu-latest - environment: - name: production - - steps: - - name: Checkout built commit - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ needs.build-metadata.outputs.commit_sha }} - - - name: Download checksum snippets - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - pattern: photos-SHA256SUMS-* - path: checksums - merge-multiple: true - - - name: Combine checksums - run: sort -k2 checksums/SHA256SUMS-* > SHA256SUMS - - - name: Point release tag at the successfully built commit - env: - COMMIT_SHA: ${{ needs.build-metadata.outputs.commit_sha }} - RELEASE_TAG: ${{ needs.build-metadata.outputs.release_tag }} - run: | - git tag --force "${RELEASE_TAG}" "${COMMIT_SHA}" - git push --force origin "refs/tags/${RELEASE_TAG}" - - - name: Mint nightly token - id: nightly_token - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 - with: - client-id: ${{ secrets.ENTE_CI_GH_CLIENT_ID }} - private-key: ${{ secrets.ENTE_CI_GH_APP_PRIVATE_KEY }} - repositories: nightly - permission-contents: write - - - name: Upload checksum to nightly release - env: - GH_TOKEN: ${{ steps.nightly_token.outputs.token }} - RELEASE_TAG: ${{ needs.build-metadata.outputs.release_tag }} - run: gh release upload "${RELEASE_TAG}" SHA256SUMS --clobber --repo ente/nightly - - - name: Upload checksum to RC draft - if: startsWith(github.ref_name, 'release/') - env: - GH_TOKEN: ${{ github.token }} - RELEASE_TAG: ${{ needs.build-metadata.outputs.release_tag }} - run: gh release upload "${RELEASE_TAG}" SHA256SUMS --clobber - - - name: Notify Discord - if: github.event_name != 'schedule' || needs.build-metadata.outputs.has_new_changes == 'true' - continue-on-error: true - env: - DISCORD_WEBHOOK: ${{ secrets.DISCORD_INTERNAL_RELEASE_WEBHOOK }} - RELEASE_TITLE: ${{ needs.build-metadata.outputs.release_title }} - RELEASE_BODY_GROUPED: ${{ needs.build-metadata.outputs.release_body_grouped }} - RELEASE_TAG: ${{ needs.build-metadata.outputs.release_tag }} - run: node .github/scripts/release-notify.mjs photos "${RELEASE_TAG}" diff --git a/.github/workflows/photos-desktop-build.yml b/.github/workflows/photos-desktop-build.yml deleted file mode 100644 index d02c1e20889..00000000000 --- a/.github/workflows/photos-desktop-build.yml +++ /dev/null @@ -1,227 +0,0 @@ -name: "Build (Photos desktop)" - -# See .github/docs/app-release.md - -on: - push: - branches: - - "release/photos-desktop-v*" - workflow_dispatch: - schedule: - # Run every weekday at ~6:45 AM IST - - cron: "15 1 * * 1-5" - -permissions: - contents: read - -concurrency: - group: photos-desktop-build - cancel-in-progress: false - -jobs: - build-metadata: - runs-on: ubuntu-latest - - outputs: - should_build: ${{ steps.prepare.outputs.should_build }} - commit_sha: ${{ steps.prepare.outputs.commit_sha }} - release_version: ${{ steps.prepare.outputs.release_version }} - release_tag: ${{ steps.prepare.outputs.release_tag }} - release_title: ${{ steps.prepare.outputs.release_title }} - release_body: ${{ steps.prepare.outputs.release_body }} - release_body_grouped: ${{ steps.prepare.outputs.release_body_grouped }} - has_new_changes: ${{ steps.prepare.outputs.has_new_changes }} - - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ github.ref_name }} - - - name: Prepare release - id: prepare - shell: bash - run: | - set -euo pipefail - - if [[ "${GITHUB_EVENT_NAME}" == "schedule" ]] && git ls-remote --exit-code --heads origin 'release/photos-desktop-v*' >/dev/null 2>&1; then - echo "Active Photos desktop release branch found; skipping scheduled nightly" - echo "should_build=false" >> "${GITHUB_OUTPUT}" - exit 0 - fi - - release_version="$(node .github/scripts/photos-desktop-version.mjs get)" - - if [[ "${GITHUB_REF_NAME}" == release/photos-desktop-v* ]]; then - if [[ "${GITHUB_REF_NAME}" != "release/photos-desktop-v${release_version}" ]]; then - echo "::error::Branch ${GITHUB_REF_NAME} does not match desktop version ${release_version}" - exit 1 - fi - release_tag="photos-desktop-v${release_version}-rc" - release_title="๐Ÿ–ฅ๏ธ Photos desktop release candidate - ${release_version}" - else - release_tag="photos-desktop-v${release_version}" - release_title="๐Ÿ–ฅ๏ธ Photos desktop nightly - ${release_version}" - fi - - git fetch --force --depth=1 --no-tags origin "refs/tags/${release_tag}:refs/tags/${release_tag}" || true - - echo "should_build=true" >> "${GITHUB_OUTPUT}" - echo "commit_sha=$(git rev-parse HEAD)" >> "${GITHUB_OUTPUT}" - echo "release_version=${release_version}" >> "${GITHUB_OUTPUT}" - echo "release_tag=${release_tag}" >> "${GITHUB_OUTPUT}" - echo "release_title=${release_title}" >> "${GITHUB_OUTPUT}" - node .github/scripts/release-notes.mjs desktop/changes "${release_tag}" >> "${GITHUB_OUTPUT}" - - build: - needs: build-metadata - if: needs.build-metadata.outputs.should_build == 'true' - runs-on: ${{ matrix.os }} - environment: - name: production - - strategy: - matrix: - os: [macos-latest, ubuntu-latest, windows-latest] - - env: - RELEASE_TAG: ${{ needs.build-metadata.outputs.release_tag }} - IS_RC: ${{ startsWith(github.ref_name, 'release/') }} - RELEASE_BODY: ${{ needs.build-metadata.outputs.release_body }} - - defaults: - run: - working-directory: desktop - - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ needs.build-metadata.outputs.commit_sha }} - - - name: Setup node - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: 24 - cache: "npm" - cache-dependency-path: "desktop/package-lock.json" - - - name: Install dependencies - run: npm ci - - - name: Fetch binary dependencies and rebuild native modules - run: npm run postinstall - - - name: Create universal ffmpeg binaries for macOS - if: startsWith(matrix.os, 'macos') - # Currently, the ffmpeg-static binaries are not universal (Not - # their fault, we thank them for their useful package, the issue - # is that there don't seem to be well known upstream sources that - # provide a universal binary). - # - # As a workaround, we invoke ffmpeg-static twice to download both - # the Intel and ARM binaries, and combine them into a single - # universal binary using lipo. - # - run: | - rm -f node_modules/ffmpeg-static/ffmpeg - npm_config_arch=arm64 node node_modules/ffmpeg-static/install.js - mv node_modules/ffmpeg-static/ffmpeg ffmpeg-arm64 - npm_config_arch=x64 node node_modules/ffmpeg-static/install.js - mv node_modules/ffmpeg-static/ffmpeg ffmpeg-x64 - lipo -create ffmpeg-arm64 ffmpeg-x64 -output node_modules/ffmpeg-static/ffmpeg - rm ffmpeg-arm64 ffmpeg-x64 - file node_modules/ffmpeg-static/ffmpeg - - - name: Install libarchive-tools for pacman build - if: startsWith(matrix.os, 'ubuntu') - # https://github.com/electron-userland/electron-builder/issues/4181 - run: sudo apt-get update && sudo apt-get install libarchive-tools - - - name: Build - run: npm run build:ci - - - name: Mint nightly token - id: nightly_token - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 - with: - client-id: ${{ secrets.ENTE_CI_GH_CLIENT_ID }} - private-key: ${{ secrets.ENTE_CI_GH_APP_PRIVATE_KEY }} - repositories: nightly,photos-desktop - permission-contents: write - - - name: Publish - shell: bash - run: | - if [ "${RUNNER_OS}" = "macOS" ]; then - export CSC_LINK="${MAC_OS_CERTIFICATE}" - export CSC_KEY_PASSWORD="${MAC_OS_CERTIFICATE_PASSWORD}" - fi - - platform=${{ startsWith(matrix.os, 'macos') && 'mac' || startsWith(matrix.os, 'windows') && 'windows' || 'linux' }} - - if [ "${IS_RC}" = "true" ]; then - npm exec -- electron-builder --$platform --publish always "-c.releaseInfo.releaseNotes=${RELEASE_BODY}" - else - npm exec -- electron-builder --$platform --publish never - fi - env: - GH_TOKEN: ${{ steps.nightly_token.outputs.token }} - MAC_OS_CERTIFICATE: ${{ secrets.MAC_OS_CERTIFICATE }} - MAC_OS_CERTIFICATE_PASSWORD: ${{ secrets.MAC_OS_CERTIFICATE_PASSWORD }} - APPLE_ID: ${{ secrets.APPLE_ID }} - APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} - AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} - AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} - # Allow rebuilds to keep updating the rolling RC draft. - EP_GH_IGNORE_TIME: true - # Workaround recommended in - # https://github.com/electron-userland/electron-builder/issues/3179 - USE_HARD_LINKS: false - - - name: Upload to nightly - uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0 - with: - repo: nightly - token: ${{ steps.nightly_token.outputs.token }} - tag: ${{ env.RELEASE_TAG }} - name: ${{ env.RELEASE_TAG }} - body: ${{ env.RELEASE_BODY }} - artifacts: "desktop/dist/ente-*.@(exe|dmg|AppImage|deb|rpm|pacman)" - prerelease: true - makeLatest: false - allowUpdates: true - updateOnlyUnreleased: true - - finish-build: - needs: [build-metadata, build] - if: needs.build-metadata.outputs.should_build == 'true' - runs-on: ubuntu-latest - permissions: - contents: write - - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ needs.build-metadata.outputs.commit_sha }} - - - name: Point release tag at the successfully built commit - env: - COMMIT_SHA: ${{ needs.build-metadata.outputs.commit_sha }} - RELEASE_TAG: ${{ needs.build-metadata.outputs.release_tag }} - run: | - git tag --force "${RELEASE_TAG}" "${COMMIT_SHA}" - git push --force origin "refs/tags/${RELEASE_TAG}" - - - name: Notify Discord - if: github.event_name != 'schedule' || needs.build-metadata.outputs.has_new_changes == 'true' - continue-on-error: true - env: - DISCORD_WEBHOOK: ${{ secrets.DISCORD_INTERNAL_RELEASE_WEBHOOK }} - RELEASE_TITLE: ${{ needs.build-metadata.outputs.release_title }} - RELEASE_BODY_GROUPED: ${{ needs.build-metadata.outputs.release_body_grouped }} - RELEASE_TAG: ${{ needs.build-metadata.outputs.release_tag }} - run: node .github/scripts/release-notify.mjs photos-desktop "${RELEASE_TAG}" diff --git a/.github/workflows/rust-e2e-test.yml b/.github/workflows/rust-e2e-test.yml deleted file mode 100644 index 4d23f559767..00000000000 --- a/.github/workflows/rust-e2e-test.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: "Test (Rust E2E)" - -on: - pull_request: - paths: - - ".github/workflows/rust-e2e-test.yml" - - "rust/**" - - "server/**" - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -env: - RUSTFLAGS: -D warnings - -jobs: - integration: - name: ${{ matrix.name }} - runs-on: ubuntu-latest - timeout-minutes: 30 - strategy: - fail-fast: false - matrix: - include: - - name: CLI integration tests - package: ente-rs - - name: E2E suite - package: ente-e2e - defaults: - run: - working-directory: rust - - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - name: Setup go - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 - with: - go-version-file: server/go.mod - - # Restore-only; primed on main by warm-caches.yml. - - name: Restore Rust cache - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - with: - workspaces: rust - shared-key: rust-cli - save-if: false - - - name: Run ${{ matrix.name }} - run: cargo test -p ${{ matrix.package }} --features museum --locked - # GITHUB_TOKEN lifts GitHub API rate limit when fetching - # postgresql_embedded binaries. - env: - GITHUB_TOKEN: ${{ github.token }} diff --git a/.github/workflows/rust-lint.yml b/.github/workflows/rust-lint.yml deleted file mode 100644 index f03ba30dace..00000000000 --- a/.github/workflows/rust-lint.yml +++ /dev/null @@ -1,58 +0,0 @@ -name: "Lint (Rust)" - -on: - pull_request: - paths: - - ".github/workflows/rust-lint.yml" - - "rust/**" - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -env: - RUSTFLAGS: -D warnings - -jobs: - lint: - runs-on: ubuntu-latest - defaults: - run: - working-directory: rust - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - name: Free runner disk space - run: sudo rm -rf /usr/share/dotnet - - - name: Install Linux dependencies - run: | - sudo apt-get update - sudo apt-get install -y libwebkit2gtk-4.1-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev libvulkan-dev glslc - - # Restore-only; primed on main by warm-caches.yml. - - name: Restore Rust cache - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - with: - workspaces: rust - shared-key: rust-workspace - save-if: false - cache-directories: rust/.cache - - - run: cargo fmt --check - - - run: cargo clippy --all-targets --locked - - - run: cargo install cargo-nextest --version 0.9.140 --locked - - - run: cargo nextest run --locked --workspace --features ente-photos/ml-assets - - - run: cargo test --locked --workspace --features ente-photos/ml-assets --doc - - - run: cargo install cargo-audit --version 0.22.2 --locked - - - run: cargo audit diff --git a/.github/workflows/self-hosted-mobile-linux.yml b/.github/workflows/self-hosted-mobile-linux.yml new file mode 100644 index 00000000000..09096f86e8b --- /dev/null +++ b/.github/workflows/self-hosted-mobile-linux.yml @@ -0,0 +1,71 @@ +name: Self-hosted mobile (Linux) + +on: + pull_request: + workflow_dispatch: + +permissions: + contents: read + pull-requests: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + validate: + name: Linux mobile gate + if: github.repository == 'vanton1/ente' + runs-on: ubuntu-24.04 + timeout-minutes: 45 + steps: + - name: Detect Linux-relevant pull-request paths + id: paths + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + if (context.eventName !== "pull_request") { + core.setOutput("relevant", "true"); + return; + } + const files = await github.paginate(github.rest.pulls.listFiles, { + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + per_page: 100, + }); + const exact = new Set([ + ".github/workflows/self-hosted-mobile-linux.yml", + "rust/Cargo.lock", + "rust/Cargo.toml", + "scripts/test_self_hosted_mobile_linux.sh", + ]); + const prefixes = [ + ".github/actions/setup-flutter/", + "mobile/", + "rust/apps/codegen/", + "rust/bindings/frb/", + "rust/crates/contacts/", + "rust/crates/core/", + "rust/crates/image/", + "rust/crates/photos/", + ]; + const relevant = files.some(({ filename }) => + exact.has(filename) || prefixes.some((prefix) => filename.startsWith(prefix)) + ); + core.setOutput("relevant", relevant ? "true" : "false"); + + - name: Checkout source and submodules + if: steps.paths.outputs.relevant == 'true' + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + submodules: recursive + + - name: Install pinned Flutter + if: steps.paths.outputs.relevant == 'true' + uses: ./.github/actions/setup-flutter + + - name: Run portable self-hosted mobile validation + if: steps.paths.outputs.relevant == 'true' + run: ./scripts/test_self_hosted_mobile_linux.sh diff --git a/.github/workflows/self-hosted-mobile-macos.yml b/.github/workflows/self-hosted-mobile-macos.yml new file mode 100644 index 00000000000..7b06385be6b --- /dev/null +++ b/.github/workflows/self-hosted-mobile-macos.yml @@ -0,0 +1,101 @@ +name: Self-hosted mobile (macOS) + +on: + pull_request: + workflow_dispatch: + +permissions: + contents: read + pull-requests: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + changes: + name: Detect macOS mobile changes + if: github.repository == 'vanton1/ente' + runs-on: ubuntu-24.04 + timeout-minutes: 5 + outputs: + relevant: ${{ steps.paths.outputs.relevant }} + steps: + - name: Detect macOS-relevant pull-request paths + id: paths + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + if (context.eventName !== "pull_request") { + core.setOutput("relevant", "true"); + return; + } + const files = await github.paginate(github.rest.pulls.listFiles, { + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + per_page: 100, + }); + const exact = new Set([ + ".github/workflows/self-hosted-mobile-macos.yml", + "mobile/pubspec.lock", + "mobile/pubspec.yaml", + "scripts/test_self_hosted_mobile_macos.sh", + ]); + const prefixes = [ + ".github/actions/setup-flutter/", + "mobile/apps/photos/", + "mobile/packages/", + ]; + const relevant = files.some(({ filename }) => + exact.has(filename) || prefixes.some((prefix) => filename.startsWith(prefix)) + ); + core.setOutput("relevant", relevant ? "true" : "false"); + + validate: + name: macOS mobile validation + needs: changes + if: github.repository == 'vanton1/ente' && needs.changes.outputs.relevant == 'true' + runs-on: macos-26 + timeout-minutes: 45 + steps: + - name: Checkout source and submodules + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + submodules: recursive + + - name: Install pinned Flutter + uses: ./.github/actions/setup-flutter + + - name: Install pinned Ruby + uses: ruby/setup-ruby@a30dfa457ad68707b8b910ac3a244714b61c0626 # v1 + with: + ruby-version: "3.3" + + - name: Install lockfile-compatible CocoaPods + run: gem install cocoapods --version 1.17.0 --no-document + + - name: Run self-hosted iOS and CocoaPods validation + run: ./scripts/test_self_hosted_mobile_macos.sh + + gate: + name: macOS mobile gate + needs: [changes, validate] + if: always() && github.repository == 'vanton1/ente' + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - name: Require relevant macOS validation + env: + CHANGES_RESULT: ${{ needs.changes.result }} + RELEVANT: ${{ needs.changes.outputs.relevant }} + VALIDATE_RESULT: ${{ needs.validate.result }} + run: | + set -euo pipefail + test "$CHANGES_RESULT" = success + if test "$RELEVANT" = true; then + test "$VALIDATE_RESULT" = success + else + test "$VALIDATE_RESULT" = skipped + fi diff --git a/.github/workflows/server-lint.yml b/.github/workflows/server-lint.yml deleted file mode 100644 index f60aea5307e..00000000000 --- a/.github/workflows/server-lint.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: "Lint (Server)" - -on: - pull_request: - paths: - - ".github/workflows/server-lint.yml" - - "server/**" - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - lint: - runs-on: ubuntu-latest - defaults: - run: - working-directory: server - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - name: Setup go - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 - with: - go-version-file: server/go.mod - cache-dependency-path: server/go.sum - cache: true - - - name: Lint - run: ./scripts/lint.sh - - - name: Test - run: ./scripts/test-with-postgres.sh docker diff --git a/.github/workflows/server-publish-ghcr.yml b/.github/workflows/server-publish-ghcr.yml deleted file mode 100644 index 83f6657db99..00000000000 --- a/.github/workflows/server-publish-ghcr.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: "Publish GHCR (Server)" - -on: - # Run automatically on 15th of every month, at 05:00 UTC. - schedule: - - cron: '0 5 15 * *' - # Run manually if needed to publish out of schedule. - workflow_dispatch: - -permissions: - contents: read - packages: write - -jobs: - publish: - runs-on: ubuntu-latest - steps: - - name: Determine commit from prod museum - run: | - museum_commit="$(curl -s https://api.ente.com/ping | jq -r .id)" - [[ "${museum_commit}" =~ ^[0-9a-f]{40}$ ]] - echo "museum_commit=${museum_commit}" >> $GITHUB_ENV - - - name: Checkout prod museum commit - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ env.museum_commit }} - - - name: Log in to GHCR - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: printf '%s' "${GITHUB_TOKEN}" | docker login ghcr.io --username "${GITHUB_ACTOR}" --password-stdin - - - name: Build and push - run: | - docker buildx create --use - docker buildx build --push \ - --file server/Dockerfile \ - --platform linux/amd64,linux/arm64 \ - --build-arg GIT_COMMIT="${museum_commit}" \ - --tag ghcr.io/ente/server:${museum_commit} \ - --tag ghcr.io/ente/server:latest \ - server diff --git a/.github/workflows/server-release.yml b/.github/workflows/server-release.yml deleted file mode 100644 index dec84e52b6b..00000000000 --- a/.github/workflows/server-release.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: "Release (Server)" - -on: - workflow_dispatch: - -permissions: - contents: read - -jobs: - build: - runs-on: ubuntu-latest - environment: production - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - name: Log in to Docker registry - env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - run: printf '%s' "${DOCKER_PASSWORD}" | docker login rg.fr-par.scw.cloud --username "${DOCKER_USERNAME}" --password-stdin - - - name: Build and push image - run: | - docker buildx build --push \ - --file server/Dockerfile \ - --build-arg GIT_COMMIT="${GITHUB_SHA}" \ - --tag rg.fr-par.scw.cloud/ente/museum-prod:${GITHUB_SHA} \ - --tag rg.fr-par.scw.cloud/ente/museum-prod:latest \ - server diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml deleted file mode 100644 index 316eea9936d..00000000000 --- a/.github/workflows/stale.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: Stale PRs - -on: - schedule: - - cron: "0 3 * * *" - workflow_dispatch: - -permissions: - issues: write - pull-requests: write - statuses: read - -jobs: - stale: - runs-on: ubuntu-latest - steps: - - name: Close pull requests missing CLA - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | - const repo = context.repo; - const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1000; - - for (const pr of await github.paginate(github.rest.pulls.list, { - ...repo, - state: "open", - per_page: 100, - })) { - if (Date.parse(pr.created_at) > cutoff) continue; - - const { data: { statuses } } = await github.rest.repos.getCombinedStatusForRef({ - ...repo, - ref: pr.head.sha, - }); - if (!statuses.some((status) => status.context === "license/cla" && status.state === "pending")) continue; - - await github.rest.issues.createComment({ - ...repo, - issue_number: pr.number, - body: "This pull request has been automatically closed because the CLA has not been signed for 7 days. Reopen it after signing.", - }); - await github.rest.pulls.update({ - ...repo, - pull_number: pr.number, - state: "closed", - }); - } - - - name: Mark and close stale pull requests - uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0 - with: - days-before-pr-stale: 30 - days-before-pr-close: 7 - days-before-issue-stale: -1 - days-before-issue-close: -1 - stale-pr-message: > - This pull request has been automatically marked as stale - because it has had no activity for 30 days. It will be - closed in 7 days if there is no further activity. - close-pr-message: > - This pull request has been automatically closed because it - remained stale for 7 days with no activity. Reopen it if - you want to continue working on it. diff --git a/.github/workflows/upstream-sync-drift.yml b/.github/workflows/upstream-sync-drift.yml index d8a0cb800f0..c612cbe853d 100644 --- a/.github/workflows/upstream-sync-drift.yml +++ b/.github/workflows/upstream-sync-drift.yml @@ -16,7 +16,7 @@ concurrency: jobs: detect: if: github.repository == 'vanton1/ente' - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 10 steps: - name: Checkout fork main diff --git a/.github/workflows/warm-caches.yml b/.github/workflows/warm-caches.yml deleted file mode 100644 index 3f6f7b7a15c..00000000000 --- a/.github/workflows/warm-caches.yml +++ /dev/null @@ -1,213 +0,0 @@ -name: "Warm caches" - -# Nightly priming of the caches used by the PR lint workflows. -# -# A PR run can only restore caches that were saved on the PR's own ref or on -# main. The lint workflows never run on main, so anything they save is scoped -# to their own PR and only churns the repository's shared 10 GB cache pool. -# Instead, this workflow (which runs on main's ref) saves the caches, and the -# lint workflows restore them without saving (save-if: false). -# -# Each job must mirror the commands, features and RUSTFLAGS of the workflow it -# primes, otherwise the saved cache will not match what that workflow needs. - -on: - schedule: - - cron: "17 2 * * *" - # For bootstrapping the caches (dispatch on main, otherwise the caches get - # saved against the wrong ref). - workflow_dispatch: - -permissions: - contents: read - -jobs: - rust-workspace: - # For rust-lint.yml. - runs-on: ubuntu-latest - defaults: - run: - working-directory: rust - env: - RUSTFLAGS: -D warnings - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - name: Free runner disk space - run: sudo rm -rf /usr/share/dotnet - - - name: Install Linux dependencies - run: | - sudo apt-get update - sudo apt-get install -y libwebkit2gtk-4.1-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev libvulkan-dev glslc - - - name: Setup Rust cache - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - with: - workspaces: rust - shared-key: rust-workspace - cache-directories: rust/.cache - - - run: cargo clippy --all-targets --locked - - - run: cargo install cargo-nextest --version 0.9.140 --locked - - - run: cargo nextest run --locked --workspace --features ente-photos/ml-assets - - - run: cargo install cargo-audit --version 0.22.2 --locked - - rust-flutter: - # For the cargo steps of mobile-lint.yml. - runs-on: ubuntu-latest - defaults: - run: - working-directory: rust - env: - RUSTFLAGS: -D warnings - steps: - - name: Checkout code and submodules - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - submodules: recursive - - - name: Install Flutter - uses: ./.github/actions/setup-flutter - - - name: Setup Rust cache - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - with: - workspaces: rust - shared-key: rust-flutter - - - run: cargo codegen frb - - - run: cargo clippy -p ente_photos_rust --features flutter --all-targets --locked - - - run: cargo clippy -p ente_rust --features flutter --all-targets --locked - - rust-wasm: - # For web-lint.yml's WASM builds. - runs-on: ubuntu-latest - defaults: - run: - working-directory: web - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - name: Setup node - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: 24 - cache: "npm" - cache-dependency-path: "web/package-lock.json" - - - name: Setup Rust cache - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - with: - workspaces: rust - shared-key: rust-wasm - - - run: npm ci - - - run: npm run build:wasm - - - run: npm run build:space-wasm - - rust-cli: - # For rust-e2e-test.yml. - runs-on: ubuntu-latest - defaults: - run: - working-directory: rust - env: - RUSTFLAGS: -D warnings - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - name: Setup go - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 - with: - go-version-file: server/go.mod - cache: false - - - name: Setup Rust cache - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - with: - workspaces: rust - shared-key: rust-cli - - - run: cargo test -p ente-rs --features museum --locked --no-run - - - run: cargo test -p ente-e2e --features museum --locked --no-run - - ensu-android: - # For ensu-android-build.yml. - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - name: Setup Rust cache - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - with: - workspaces: rust - shared-key: ensu-android - - - name: Install Vulkan build dependencies - run: | - sudo apt-get update - sudo apt-get install -y libvulkan-dev glslc - - - run: cargo codegen native - working-directory: rust - - - name: Build Ensu - working-directory: mobile/native/android/apps/ensu - run: ./gradlew :app:assembleDebug - - ensu-ios: - # For ensu-ios-build.yml. - runs-on: macos-26 - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - name: Setup Rust cache - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - with: - workspaces: rust - shared-key: ensu-ios - - - run: cargo codegen native - working-directory: rust - - # Compile the app and tests without running them. - - name: Build Ensu - working-directory: mobile/native/apple/apps/ensu - run: xcodebuild build-for-testing -scheme Ensu -destination 'platform=iOS Simulator,name=iPhone 17' - - go-server: - # For server-lint.yml. - runs-on: ubuntu-latest - defaults: - run: - working-directory: server - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - name: Setup go - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 - with: - go-version-file: server/go.mod - cache-dependency-path: server/go.sum - cache: true - - - name: Install dependencies - run: sudo apt-get update && sudo apt-get install libsodium-dev - - - name: Lint - run: "./scripts/lint.sh" diff --git a/.github/workflows/web-crowdin-push-sources-and-translations.yml b/.github/workflows/web-crowdin-push-sources-and-translations.yml deleted file mode 100644 index cc35d4e4b1c..00000000000 --- a/.github/workflows/web-crowdin-push-sources-and-translations.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: "Push sources and translations to Crowdin (Web)" - -# This is a variant of web-crowdin-sync.yml that uploads the translated strings in -# addition to the source strings. -# -# This allows us to change the strings in our source code for an automated -# refactoring (e.g. renaming a key), and then run this workflow to update the -# data in Crowdin taking our source code as the source of truth. - -# USAGE: -# -# Be careful with this workflow since it updates both the sources and -# translations on Crowdin, and any changes on Crowdin that are not in the -# branch from which this workflow is run will be lost. -# -# 1. Run the normal sync workflow: -# `gh workflow run web-crowdin-sync.yml` -# -# 2. Merge the translations PR it creates. So now `main` includes the latest -# Crowdin translations. -# -# 3. Branch from updated `main` and make the string edits. -# -# 4. Push the branch and run this workflow -# `gh workflow run web-crowdin-push-sources-and-translations.yml --ref ` -# -# 5. After it succeeds, merge the string-edit PR. - -on: - # Trigger manually, or using - # `gh workflow run web-crowdin-push-sources-and-translations.yml --ref ` - workflow_dispatch: - -permissions: - contents: read - -jobs: - push-sources-and-translations-to-crowdin: - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - name: Crowdin push - uses: crowdin/github-action@52aa776766211d83d975df51f3b9c53c2f8ba35f # v2.16.3 - with: - base_path: "web/" - config: "web/crowdin.yml" - upload_sources: true - upload_translations: true - download_translations: false - project_id: 569613 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} diff --git a/.github/workflows/web-crowdin-sync.yml b/.github/workflows/web-crowdin-sync.yml deleted file mode 100644 index 3c5caf7f700..00000000000 --- a/.github/workflows/web-crowdin-sync.yml +++ /dev/null @@ -1,58 +0,0 @@ -name: "Sync Crowdin (Web)" - -on: - push: - branches: [main] - paths: - - ".github/workflows/web-crowdin-sync.yml" - - "web/packages/base/locales/en-US/translation.json" - - "web/crowdin.yml" - schedule: - # Run Mondays at ~6:00 AM IST - # - # [Note: Run workflow on specific days of the week] - # - # The last (5th) component of the cron syntax denotes the day of the - # week, with 0 == SUN and 6 == SAT. So, for example, to run on every TUE - # and FRI, this can be set to `2,5`. - # - # See also: [Note: Run workflow every 24 hours] - - cron: "20 0 * * 1" - # Also allow manually running the workflow. - workflow_dispatch: - -# "In order to push translations and create pull requests, the Crowdin GitHub -# action requires the `GITHUB_TOKEN` to have write permission on the `contents` -# and `pull-requests`. -# -# - https://github.com/crowdin/github-action?tab=readme-ov-file#permissions -permissions: - contents: write - pull-requests: write - -jobs: - synchronize-with-crowdin: - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - name: Crowdin's action - uses: crowdin/github-action@52aa776766211d83d975df51f3b9c53c2f8ba35f # v2.16.3 - with: - base_path: "web/" - config: "web/crowdin.yml" - upload_sources: true - upload_translations: false - download_translations: true - localization_branch_name: translations/web - create_pull_request: true - skip_untranslated_strings: true - pull_request_title: "[web] New translations" - pull_request_body: "New translations from [Crowdin](https://crowdin.com/project/ente-photos-web)" - pull_request_base_branch_name: "main" - project_id: 569613 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} diff --git a/.github/workflows/web-deploy-2of3.yml b/.github/workflows/web-deploy-2of3.yml deleted file mode 100644 index 28a79bb2c54..00000000000 --- a/.github/workflows/web-deploy-2of3.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: "Deploy (2of3)" - -on: - workflow_dispatch: - -permissions: - contents: read - -jobs: - deploy: - runs-on: ubuntu-latest - environment: production - - defaults: - run: - working-directory: web - - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - name: Setup node - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: 24 - cache: "npm" - cache-dependency-path: "web/package-lock.json" - - - name: Install dependencies - run: npm ci - - - name: Audit dependencies - run: npm audit --audit-level critical - - - name: Build 2of3 - run: npm run build:twoof3 - - - name: Publish 2of3 - uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0 - with: - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - command: pages deploy --project-name=ente --commit-dirty=true --branch=deploy/2of3 web/apps/twoof3/out diff --git a/.github/workflows/web-deploy.yml b/.github/workflows/web-deploy.yml deleted file mode 100644 index 27f9b834506..00000000000 --- a/.github/workflows/web-deploy.yml +++ /dev/null @@ -1,147 +0,0 @@ -name: "Deploy (Web)" - -on: - schedule: - # [Note: Run workflow every 24 hours] - # - # Run every weekday at ~8:00 AM IST. - # - # First field is minute, second is hour of the day. Last is day of week, - # 0 being Sunday. - # - # Add a few minutes of offset to avoid scheduling on exact hourly - # boundaries (recommended by GitHub to avoid congestion). - # - # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule - # https://crontab.guru/ - # - - cron: "25 2 * * 1-5" - # Also allow manually running the workflow - workflow_dispatch: - -permissions: - contents: read - -jobs: - deploy: - runs-on: ubuntu-latest - environment: production - - defaults: - run: - working-directory: web - - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - name: Setup node - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: 24 - cache: "npm" - cache-dependency-path: "web/package-lock.json" - - - name: Install dependencies - run: npm ci - - - name: Audit dependencies - run: npm audit --audit-level critical - - name: Deploy photos - uses: ./.github/actions/deploy-web-app - with: - app: photos - account-id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }} - - - name: Deploy albums - uses: ./.github/actions/deploy-web-app - with: - app: albums - account-id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }} - - - name: Deploy accounts - uses: ./.github/actions/deploy-web-app - with: - app: accounts - account-id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }} - - - name: Deploy auth - uses: ./.github/actions/deploy-web-app - with: - app: auth - account-id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }} - - - name: Deploy legacy - uses: ./.github/actions/deploy-web-app - with: - app: legacy - account-id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }} - - - name: Deploy cast - uses: ./.github/actions/deploy-web-app - with: - app: cast - account-id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }} - - - name: Deploy payments - uses: ./.github/actions/deploy-web-app - with: - app: payments - account-id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }} - output: dist - - - name: Deploy locker - uses: ./.github/actions/deploy-web-app - with: - app: locker - account-id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }} - - - name: Deploy share - uses: ./.github/actions/deploy-web-app - with: - app: share - account-id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }} - - - name: Deploy space - uses: ./.github/actions/deploy-web-app - with: - app: space - account-id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }} - - - name: Deploy memories - uses: ./.github/actions/deploy-web-app - with: - app: memories - account-id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }} - - - name: Deploy paste - uses: ./.github/actions/deploy-web-app - with: - app: paste - account-id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }} - - - name: Deploy embed - uses: ./.github/actions/deploy-web-app - with: - app: embed - account-id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }} - - - name: Deploy ensu - uses: ./.github/actions/deploy-web-app - with: - app: ensu - account-id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }} diff --git a/.github/workflows/web-lint.yml b/.github/workflows/web-lint.yml deleted file mode 100644 index 007abd37d84..00000000000 --- a/.github/workflows/web-lint.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: "Lint (Web)" - -on: - # Run on every pull request (open or push to it) that changes web/ - pull_request: - paths: - - "web/**" - - "rust/bindings/wasm/ente-wasm/**" - - "rust/bindings/wasm/space/**" - - "rust/space/**" - - "rust/crates/contacts/**" - - "rust/crates/core/**" - - "rust/Cargo.lock" - - "rust/Cargo.toml" - - ".github/workflows/web-lint.yml" - -permissions: - contents: read - -# Cancel in-progress lint runs when a new commit is pushed. -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - lint: - runs-on: ubuntu-latest - defaults: - run: - working-directory: web - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - name: Setup node - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: 24 - cache: "npm" - cache-dependency-path: "web/package-lock.json" - - # Restore-only; primed on main by warm-caches.yml. - - name: Restore Rust cache - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - with: - workspaces: rust - shared-key: rust-wasm - save-if: false - - - run: npm ci - - - run: npm audit --audit-level critical - - - run: npm run build:wasm - - - run: npm run build:space-wasm - - - run: npm run lint - - - run: npm test diff --git a/.github/workflows/web-publish-ghcr.yml b/.github/workflows/web-publish-ghcr.yml deleted file mode 100644 index 61782135049..00000000000 --- a/.github/workflows/web-publish-ghcr.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: "Publish GHCR (Web)" - -on: - # Run automatically every Wednesday, at 07:00 UTC. - schedule: - - cron: '0 7 * * 3' - # Run manually if needed to publish out of schedule. - workflow_dispatch: - -permissions: - contents: read - packages: write - -jobs: - publish: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - name: Log in to GHCR - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: printf '%s' "${GITHUB_TOKEN}" | docker login ghcr.io --username "${GITHUB_ACTOR}" --password-stdin - - - name: Build and push - run: | - docker buildx create --use - docker buildx build --push \ - --file web/Dockerfile \ - --platform linux/amd64,linux/arm64 \ - --tag ghcr.io/ente/web:${GITHUB_SHA} \ - --tag ghcr.io/ente/web:latest \ - . diff --git a/.github/workflows/workflow-security-checks.yml b/.github/workflows/workflow-security-checks.yml index c51f0438b2b..1130d5c4c77 100644 --- a/.github/workflows/workflow-security-checks.yml +++ b/.github/workflows/workflow-security-checks.yml @@ -2,31 +2,83 @@ name: Workflow security checks on: pull_request: - paths: - - '.github/workflows/**' - - '.github/actions/**' - - '.github/scripts/check_workflow_security.rb' permissions: contents: read + pull-requests: read jobs: - workflow-security-checks: - runs-on: ubuntu-latest + changes: + name: Detect workflow changes + if: github.repository == 'vanton1/ente' + runs-on: ubuntu-24.04 + timeout-minutes: 5 + outputs: + relevant: ${{ steps.paths.outputs.relevant }} + steps: + - name: Detect automation-policy changes + id: paths + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const files = await github.paginate(github.rest.pulls.listFiles, { + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + per_page: 100, + }); + const relevant = files.some(({ filename }) => + filename.startsWith(".github/actions/") || + filename.startsWith(".github/workflows/") || + filename === ".github/scripts/check_workflow_security.rb" || + filename === ".github/scripts/check_workflow_security_test.rb" + ); + core.setOutput("relevant", relevant ? "true" : "false"); + + validate: + name: Workflow security validation + needs: changes + if: github.repository == 'vanton1/ente' && needs.changes.outputs.relevant == 'true' + runs-on: ubuntu-24.04 + timeout-minutes: 10 environment: # Require admin approval for workflow changes. name: workflow-change-approval steps: - name: Checkout PR uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false - name: Checkout trusted checker uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ github.event.pull_request.base.sha }} path: trusted + persist-credentials: false sparse-checkout: .github/scripts/check_workflow_security.rb sparse-checkout-cone-mode: false - name: Run workflow security checks run: ruby trusted/.github/scripts/check_workflow_security.rb + + gate: + name: Workflow security gate + needs: [changes, validate] + if: always() && github.repository == 'vanton1/ente' + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - name: Require relevant workflow validation + env: + CHANGES_RESULT: ${{ needs.changes.result }} + RELEVANT: ${{ needs.changes.outputs.relevant }} + VALIDATE_RESULT: ${{ needs.validate.result }} + run: | + set -euo pipefail + test "$CHANGES_RESULT" = success + if test "$RELEVANT" = true; then + test "$VALIDATE_RESULT" = success + else + test "$VALIDATE_RESULT" = skipped + fi diff --git a/FORK.md b/FORK.md index 311ce51730b..a48fad8bb50 100644 --- a/FORK.md +++ b/FORK.md @@ -21,6 +21,7 @@ supported by Ente, and its mobile builds are not official Ente releases. | Distribution | Ente's official distribution channels | Closed-group Android and iOS distribution through guarded Firebase App Distribution workflows; iOS uses Apple Ad Hoc device provisioning | | Release tooling | Upstream release processes | Fork-specific build, audit, immutable preparation, confirmation, publication, receipt, and recovery scripts | | Upstream maintenance | Source of official Ente development | Daily/manual drift reporting plus a guarded local synchronization command that opens a reviewable pull request and never merges it automatically | +| GitHub Actions | Official Ente production, product, deployment, and release automation | An exact fork-owned allowlist for self-hosted mobile, dependency/workflow security, and upstream drift; no signing, publication, deployment, or operational secrets | Most fork-specific runtime changes are limited to Ente Photos mobile. The rest of the monorepo follows upstream unless a compatibility, documentation, test, @@ -101,6 +102,13 @@ operator procedure and the [upstream synchronization architecture](living_docs/UpstreamEnteSynchronizationArchitecture.md) for its provenance and safety model. +Pull requests into fork `main` must pass five fork-owned GitHub Actions gates. +Expensive mobile and CodeQL work runs only for relevant changes, while stable +checks still report for documentation-only pull requests. See the +[fork GitHub Actions architecture](living_docs/ForkGitHubActionsArchitecture.md) +for the exact allowlist, permissions, branch protection, and upstream-adoption +procedure. + ## Documentation map Start with the @@ -112,7 +120,8 @@ It routes each audience to the current canonical guide: - [iOS closed-beta operations](mobile/apps/photos/SELF_HOSTED_IOS_DISTRIBUTION_GUIDE.md); - [Android and iOS tester onboarding](mobile/apps/photos/SELF_HOSTED_TESTER_ONBOARDING_GUIDE.md); - [configurable-server architecture](living_docs/ConfigurableSelfHostedMobileServerArchitecture.md); and -- [upstream synchronization](UPSTREAM_SYNC.md). +- [upstream synchronization](UPSTREAM_SYNC.md); and +- [fork GitHub Actions architecture](living_docs/ForkGitHubActionsArchitecture.md). Files under `living_docs/` preserve implementation decisions and acceptance evidence. Unless explicitly marked as current architecture, they are historical diff --git a/UPSTREAM_SYNC.md b/UPSTREAM_SYNC.md index de066e7af7e..8f6317c3745 100644 --- a/UPSTREAM_SYNC.md +++ b/UPSTREAM_SYNC.md @@ -9,6 +9,8 @@ For the design and security boundaries behind these commands, see the [upstream synchronization architecture](living_docs/UpstreamEnteSynchronizationArchitecture.md). The first large manual catch-up remains available as historical evidence in the [implementation record](living_docs/UpstreamEnteSynchronization.md). +The pull request created by this runbook is enforced by the five checks in the +[fork GitHub Actions architecture](living_docs/ForkGitHubActionsArchitecture.md). ## 1. Automated drift notice diff --git a/living_docs/ForkGitHubActions.md b/living_docs/ForkGitHubActions.md new file mode 100644 index 00000000000..e91864366a6 --- /dev/null +++ b/living_docs/ForkGitHubActions.md @@ -0,0 +1,503 @@ +# Fork GitHub Actions Maintenance + +**Status:** Complete. The tracker and decision history are retained as implementation evidence. +**Started:** 2026-07-22 +**Owner:** vanton +**Planning doc:** n/a +**Companion docs:** [Fork overview](../FORK.md), [self-hosted mobile documentation index](../mobile/apps/photos/SELF_HOSTED_DOCUMENTATION.md), [upstream synchronization runbook](../UPSTREAM_SYNC.md), [upstream synchronization architecture](UpstreamEnteSynchronizationArchitecture.md), [fork GitHub Actions architecture](ForkGitHubActionsArchitecture.md) + +--- + +## 1. Phase / Task tracker + +| Phase | Task | Title | Size | Status | Notes | +|------:|----:|-------|:----:|--------|-------| +| 1 | 1.1 | Inventory every inherited workflow and record an evidence-based disposition | M | ๐ŸŸข done | Audited all 38 active workflow files, their triggers, permissions, secret/service dependencies, recent runs, representative failure logs, and fork relevance. The disposition is 23 risk-first removals in Task 1.2, nine unrelated check removals in Task 3.2, four fork-focused replacements/repairs, and two retained fork workflows to harden. No workflow or GitHub setting changed. | +| 1 | 1.2 | Remove inherited deployment, release, translation, and scheduled runner automation | M | ๐ŸŸข done | Deleted the 23 audited upstream release, deployment, translation, container-publication, cache-warming, stale-PR, and scheduled product-build workflows. The 15 intentionally deferred or retained checks still parse; workflow security passes across the remaining 17 workflow/action files. No GitHub setting or external application, server, signing, distribution, issue, or pull-request state changed. | +| 2 | 2.1 | Add Linux CI for the self-hosted Photos mobile behavior and source quality | M | ๐ŸŸข done | Replaced the inherited broad mobile lint with a least-privilege Linux workflow and one reproducible script covering standard, configurable, and locked endpoint modes, Linux-portable Android release contracts, generated Rust bindings, tracked-Dart formatting, and full mobile analysis. Local proof: 181 focused tests passed across the three modes, formatting was unchanged, analysis reported no issues, the workflow parses, and the workflow-security contract passes. | +| 2 | 2.2 | Add macOS CI for the self-hosted iOS contracts and deterministic CocoaPods state | M | ๐ŸŸข done | Replaced the all-app Podfile check with a Photos-only macOS workflow that pins Flutter, Ruby, and CocoaPods 1.17.0, runs the four Apple-tool/release contracts, and verifies `pod install --deployment` without signing or publication. Local proof: all 51 iOS tests passed, the pinned Podfile installed with no tracked changes, the workflow parses, and workflow security passes. | +| 2 | 2.3 | Preserve and harden upstream-drift and workflow-security checks | S | ๐ŸŸข done | Kept the two fork-owned maintenance workflows, moved both to a fixed Ubuntu image, added the exact fork guard and timeout to the trusted workflow checker, and disabled persisted checkout credentials in both of its checkouts. The drift job remains schedule/manual only with the minimum `contents: read` and `issues: write` permissions. All 42 Ruby/Node drift contracts and 187 assertions passed with the workflow-security scan. | +| 3 | 3.1 | Repair dependency review and retain only useful security scanning | M | ๐ŸŸข done | Enabled GitHub vulnerability alerts and the dependency graph, then narrowed dependency review to Photos mobile Pub/Rust manifests and pinned workflow actions. The dependency graph now exports 3,902 packages and its comparison API succeeds. Replaced broad Go/JavaScript/Actions CodeQL with least-privilege Actions-only PR, weekly, and manual scanning; both workflows use fixed runners, timeouts, exact fork guards, SHA-pinned actions, and no secrets. | +| 3 | 3.2 | Remove remaining unrelated product and monorepo checks | S | ๐ŸŸข done | Deleted the nine audited read-only but unrelated desktop, documentation-site, Ensu Android/iOS, staff-infrastructure, broad Rust, server, and web checks. Exactly six intended workflows remain; all parse and the current security checker passes across the six workflows and two local actions. | +| 3 | 3.3 | Enforce the exact fork workflow allowlist and security contract | M | ๐ŸŸข done | Replaced the advisory checker with an exact contract for six workflow files and the pinned Flutter setup action, and removed the now-unused inherited web-deployment action. The checker rejects missing/unexpected automation, trigger or permission drift, secrets, unpinned actions, persisted checkout credentials, mutable runner labels, missing fork guards/timeouts, and unapproved environments. Five fixture tests with 37 assertions prove pass and fail cases; the complete drift/security suite remains green. | +| 4 | 4.1 | Validate the complete workflow set locally and in controlled GitHub runs | M | ๐ŸŸข done | Local YAML, shell, allowlist, five checker fixtures/37 assertions, 42 drift contracts/189 Ruby assertions, endpoint modes, formatting, analysis, iOS contracts, and CocoaPods checks passed. Relevant-path PR #5 passed all five uniquely named gates (Linux 13m56s; macOS 5m43s; CodeQL 44s; dependency 8s; workflow security 11s). Disposable Markdown-only PR #6 returned all five green gates in 2โ€“5s while skipping heavy validation. Main protection now requires the exact GitHub Actions checks, strict freshness, admin enforcement, and resolved conversations; force pushes/deletion are disabled. | +| 4 | 4.2 | Document the as-built fork GitHub Actions architecture | S | ๐ŸŸข done | Added `ForkGitHubActionsArchitecture.md` with the final allowlist, triggers, path/gate flow, responsibilities, permissions, branch settings, failures, rollback, upstream adoption, and live evidence. Linked it from the fork overview, self-hosted documentation index, upstream runbook, and this record; local-link and privacy validation passed. | + +**Legend:** โšช not started ยท ๐ŸŸก working ยท ๐ŸŸข done ยท ๐Ÿ”ด blocked / needs decision +**Size:** XS ยท S ยท M ยท L ยท XL (never days or weeks). + +One task = one row = one reviewable step. Mark a row ๐ŸŸก working before starting +and ๐ŸŸข done only after its acceptance evidence passes. Task naming follows +`Task . โ€” `; commits, when requested, mirror +that title. + +--- + +## 2. Goal + +Give the owner and contributors of this self-hosted Ente Photos mobile fork a +quiet, trustworthy GitHub Actions surface. V1 is complete when every inherited +workflow has an evidence-based disposition; workflows tied to Ente production, +unrelated products, scheduled builds, translations, signing, or deployment are +gone; fork-owned mobile, dependency, workflow-security, and upstream-drift +checks pass on the paths they protect; relevant failures block merging; and an +exact allowlist prevents a future upstream synchronization from silently +reactivating automation. + +Success is observable in GitHub: relevant pull requests receive only useful, +actionable checks; irrelevant changes do not consume expensive runners; the +Actions page no longer fills with expected missing-secret or missing-service +failures; the upstream drift workflow continues to manage its single tracking +issue; and no retained workflow can sign, publish, deploy, translate, or mutate +private operational state. + +The work targets the fork owner and occasional contributors reviewing changes +or upstream synchronization pull requests. It does not recreate official +Ente's CI, release infrastructure, or organization secrets. There is no +application-runtime latency or throughput objective; CI must provide bounded, +non-duplicated feedback only for relevant changes. + +--- + +## 3. Architecture / approach + +The selected architecture replaces inherited automation with a small, +fork-owned allowlist: + +```text +pull request or manual check + | + +--> Linux self-hosted mobile validation + +--> macOS iOS contract + CocoaPods validation + +--> dependency/security validation + +--> workflow allowlist + security validation + +daily/manual detector + | + +--> read official upstream history + +--> create/update/close one fork drift issue + +anything else + | + +--> no workflow file, no runner, no secret, no mutation +``` + +The intended retained set has separate responsibilities: + +- a Linux workflow owns fork-specific Photos endpoint and release-tool tests, + tracked-Dart formatting, and complete mobile analysis; +- a macOS workflow owns tests requiring Apple command-line tools and + deterministic CocoaPods verification, without building a signed artifact; +- dependency review and only demonstrably useful code scanning own third-party + and supported-language security evidence; +- the existing workflow-security check owns action pinning, minimal + permissions, safe checkout/authentication, and the exact workflow allowlist; + and +- the existing upstream-drift workflow retains its fork identity guard and + narrow `contents: read` plus `issues: write` role. + +Every retained pull-request check fails closed. External actions stay pinned to +full commit SHAs. Jobs default to read-only repository permissions and do not +receive signing, Firebase, Apple, Cloudflare, Crowdin, Tailscale, Museum, +container-registry, app-store, or deployment credentials. Pull requests are +treated as untrusted input; workflows must not execute mutable privileged +operations on their code. + +Path filters and job boundaries keep Linux-only work off macOS and avoid +unrelated monorepo activity. Platform-specific tests run on the platform that +provides their real tools rather than weakening production scripts or tests to +make them pass on an incompatible runner. The allowlist is checked from the +repository itself so a later upstream merge that restores or adds a workflow +fails review before that workflow is accepted into fork `main`. + +The implementation is sequenced risk-first. It records a complete inventory, +then removes production and scheduled automation, introduces the replacement +checks, repairs supported dependency security, removes the remaining unrelated +checks, enforces the final allowlist, and finishes with controlled live proof. +Each source change is reversible with Git. Repository security settings changed +for dependency review must also be individually reversible and recorded. + +Preliminary evidence from 2026-07-21 and 2026-07-22 already shows the central +failure classes: Crowdin runs lack the official API token; Ente deployment and +release workflows expect official environments, signing material, or service +credentials; dependency review reports that the repository feature is not +enabled; and the broad Linux mobile lint invokes iOS-only `plutil` tests and +finds fork release-directory permission assumptions. Task 1.1 will turn the +sample into a complete, reviewable disposition matrix instead of treating it +as the final audit. + +### Task 1.1 workflow disposition inventory + +The inventory was captured on 2026-07-22 from the local YAML, the GitHub +workflow API, the latest available 100 fork runs, failed job/step metadata, and +representative failed logs from 2026-07-20 through 2026-07-22. GitHub reported +all 38 files as active. โ€œNo recent runโ€ means no execution appeared in that +bounded fork history; it does not claim that the official repository never ran +the workflow. Secret names were classified without reading secret values. + +| Workflow | Trigger and access boundary | Fork evidence | Disposition and reason | +|---|---|---|---| +| `app-release.yml` | Manual; `contents: write`, `pull-requests: write`; official GitHub App credentials | No recent fork run | **Remove in Task 1.2.** Official multi-application release orchestration is outside the fork and can mutate releases and pull requests. | +| `auth-build.yml` | Push, manual, schedule; `contents: write`; Android, Apple, Windows, Firebase, Azure, GitHub App, and notification secrets | Two scheduled failures; signing, token minting, and certificate steps failed | **Remove in Task 1.2.** Auth builds and official signed releases are unrelated and depend on unavailable production credentials. | +| `cli-release.yml` | Version-tag push; read-only token plus release commands; official-repository condition present inside the job | No recent fork run | **Remove in Task 1.2.** The fork does not publish the Ente CLI, and retaining dormant release logic adds unnecessary surface. | +| `codeql.yml` | Manual and weekly schedule; job grants `security-events: write` and `packages: read`; no repository secrets | No recent fork run | **Replace/adapt in Task 3.1.** Actions scanning is relevant, but broad Go and JavaScript/TypeScript analysis must earn its runner cost against fork scope. | +| `copycat-db-release.yml` | Manual; `contents: read`; external container-registry username/password | No recent fork run | **Remove in Task 1.2.** Publishing the server database image is unrelated and requires external release credentials. | +| `dependency-review.yml` | Dependency-changing pull requests; `contents: read`; no secrets | One PR failure: dependency review is unsupported until the repository dependency graph is enabled | **Repair in Task 3.1.** Dependency risk is in scope, but repository support and useful manifest coverage must be enabled and proven. | +| `desktop-lint.yml` | Desktop-changing pull requests; `contents: read`; no secrets | No recent fork run | **Remove in Task 3.2.** Desktop Photos is outside the self-hosted Android/iOS client boundary. | +| `docs-deploy-redirect.yml` | Docs redirect push and manual; `contents: read`; Cloudflare credentials | No recent fork run | **Remove in Task 1.2.** The fork does not control the official help-domain redirect or its Cloudflare account. | +| `docs-deploy.yml` | Docs push and manual; `contents: read`; production environment and Cloudflare credentials | One push failure in the publish step | **Remove in Task 1.2.** Official documentation-site deployment is not owned by the fork. | +| `docs-verify-build.yml` | Docs-changing pull requests; `contents: read`; no secrets | No recent fork run | **Remove in Task 3.2.** The official documentation site is outside V1; fork Markdown contracts will live in the replacement validation. | +| `ensu-android-build.yml` | Ensu Android pull requests; `contents: read`; no secrets | One successful PR run | **Remove in Task 3.2.** A green but unrelated Ensu build still consumes runners and does not protect self-hosted Photos. | +| `ensu-build.yml` | Push, manual, schedule; `contents: write`; signing, Apple, Azure, Firebase, GitHub App, updater, and notification secrets | Two scheduled failures across signed Android, iOS, and desktop jobs | **Remove in Task 1.2.** Official Ensu release automation is unrelated and credential-bound. | +| `ensu-ios-build.yml` | Ensu iOS pull requests on macOS; `contents: read`; no secrets | One successful PR run | **Remove in Task 3.2.** It consumes an expensive runner for an unrelated application. | +| `infra-deploy-staff.yml` | Staff-site push and manual; `contents: read`; production environment and Cloudflare credentials | One push failure in the publish step | **Remove in Task 1.2.** The fork neither owns nor deploys Ente staff infrastructure. | +| `infra-lint-staff.yml` | Staff-infrastructure pull requests; `contents: read`; no secrets | No recent fork run | **Remove in Task 3.2.** Staff infrastructure is unrelated to mobile self-hosting. | +| `locker-build.yml` | Push, manual, schedule; `contents: write`; Android, Apple, Firebase, GitHub App, and notification secrets | Two scheduled failures in Android and Apple signing steps | **Remove in Task 1.2.** Locker and its official signed distribution are outside fork scope. | +| `mobile-crowdin-push-sources-and-translations.yml` | Manual; `contents: read`; Crowdin token and GitHub token | No recent fork run | **Remove in Task 1.2.** The fork does not own the official Crowdin project or translation publication flow. | +| `mobile-crowdin-push-sources.yml` | Mobile source push; `contents: read`; Crowdin token and GitHub token | Two push failures; required Crowdin API token was absent | **Remove in Task 1.2.** It fails by design without the official translation service and runs on accepted fork changes. | +| `mobile-crowdin-sync.yml` | Manual and schedule; `contents: write`, `pull-requests: write`; Crowdin token | No recent fork run | **Remove in Task 1.2.** It can create translation commits/PRs using an official service the fork does not own. | +| `mobile-lint.yml` | Mobile-changing PR and manual; `contents: read`; no secrets; broad Linux Flutter/Rust/test workload | Two PR failures; latest run passed 416 tests but failed 12 fork release-tool tests because Linux lacks `plutil` and two temporary directories were not mode `0700` | **Replace in Task 2.1.** Formatting, analysis, and fork tests matter, but platform-specific contracts must not be forced through one broad Linux test sweep. | +| `mobile-podfile-lock.yml` | Mobile dependency/iOS changes and manual; `contents: read`; no secrets; macOS | One PR failure; Photos failed first because deployment mode recalculated Flutter plugin podspec checksums | **Replace in Task 2.2.** Deterministic self-hosted iOS dependencies matter, but the fork needs a focused, reproducible Photos contract rather than all official mobile apps. | +| `photos-build.yml` | Push, manual, schedule; `contents: write`; Android, Apple, Firebase, GitHub App, and notification secrets | Two scheduled failures; Android signing material was invalid/absent and iOS pod installation failed | **Remove in Task 1.2.** This is official signed Photos release automation, not the fork's guarded local build/distribution path. | +| `photos-desktop-build.yml` | Push, manual, schedule; nominally `contents: read`; GitHub App, Apple, Azure, and notification secrets | Two scheduled failures; nightly token minting failed | **Remove in Task 1.2.** Desktop release automation is unrelated and depends on official credentials. | +| `rust-e2e-test.yml` | Rust-changing pull requests; `contents: read`; no secrets | One successful PR run | **Remove in Task 3.2.** Broad Rust end-to-end server coverage exceeds the selected mobile-fork boundary; mobile-used Rust generation/analysis belongs in replacement CI. | +| `rust-lint.yml` | Rust-changing pull requests; `contents: read`; no secrets | One successful PR run | **Remove in Task 3.2.** Repository-wide Rust lint is outside V1; only Rust paths required by Photos mobile will be exercised by replacement validation. | +| `server-lint.yml` | Server-changing pull requests; `contents: read`; no secrets | One successful PR run | **Remove in Task 3.2.** The fork consumes a self-hosted server but does not maintain a divergent server product. | +| `server-publish-ghcr.yml` | Manual and schedule; `contents: read`, `packages: write`; GitHub token | No recent fork run | **Remove in Task 1.2.** Publishing server containers is an external mutation outside the local mobile fork. | +| `server-release.yml` | Manual; `contents: read`; external container-registry username/password | No recent fork run | **Remove in Task 1.2.** Official server release publication is not owned by the fork. | +| `stale.yml` | Manual and schedule; `issues: write`, `pull-requests: write`, `statuses: read`; no secrets | Two successful scheduled runs | **Remove in Task 1.2.** Automatic upstream PR lifecycle mutation is unnecessary for this low-volume personal fork. | +| `upstream-sync-drift.yml` | Daily and manual; `contents: read`, `issues: write`; exact fork guard; no secrets | No recent run in the bounded history; deterministic local contracts and prior implementation validation pass | **Keep and harden in Task 2.3.** It is fork-owned, reports official drift through one issue, and cannot change source or releases. | +| `warm-caches.yml` | Manual and schedule; `contents: read`; seven Linux/macOS cache jobs; no secrets | Two scheduled failures; latest failure occurred in Go server lint | **Remove in Task 1.2.** It spends runners across unrelated products to support upstream CI that the fork is removing. | +| `web-crowdin-push-sources-and-translations.yml` | Manual; `contents: read`; Crowdin token and GitHub token | No recent fork run | **Remove in Task 1.2.** The official web translation project is outside fork ownership. | +| `web-crowdin-sync.yml` | Web push, manual, schedule; `contents: write`, `pull-requests: write`; Crowdin token | One push failure consistent with absent official Crowdin credentials | **Remove in Task 1.2.** It can author source/PR changes through an unavailable official service. | +| `web-deploy-2of3.yml` | Manual; `contents: read`; production environment and Cloudflare credentials | No recent fork run | **Remove in Task 1.2.** The fork does not deploy the official 2of3 web application. | +| `web-deploy.yml` | Manual and schedule; `contents: read`; production environment and Cloudflare credentials | Two scheduled failures; latest failed while installing/running Wrangler | **Remove in Task 1.2.** Official web deployment is unrelated even when a failure is an upstream tool issue rather than only a missing secret. | +| `web-lint.yml` | Web-changing pull requests; `contents: read`; no secrets | One successful PR run | **Remove in Task 3.2.** A successful but unrelated web lint does not protect the self-hosted mobile applications. | +| `web-publish-ghcr.yml` | Manual and schedule; `contents: read`, `packages: write`; GitHub token | No recent fork run | **Remove in Task 1.2.** Publishing web containers is an external mutation outside fork scope. | +| `workflow-security-checks.yml` | Workflow/action-changing pull requests; `contents: read`; protected approval environment; no secrets | One successful PR run; the same trusted checker passes locally across all 40 workflow/action files | **Keep and harden in Tasks 2.3 and 3.3.** It is fork-owned and already protects action pinning and permissions; it will also enforce the final allowlist. | + +The final count is internally consistent: 23 workflows are removed in the +risk-first Task 1.2; nine read-only but unrelated checks are removed in Task +3.2; `mobile-lint.yml`, `mobile-podfile-lock.yml`, `dependency-review.yml`, and +`codeql.yml` are replaced or adapted; and `upstream-sync-drift.yml` plus +`workflow-security-checks.yml` are retained and hardened. The replacement +Linux and macOS fork workflows do not exist yet and therefore are not included +in the 38-file inherited inventory. + +--- + +## 4. Future work / out-of-scope + +> Single source of truth for everything that is NOT in V1. Items move into V1 +> only with explicit owner approval and a decision-log entry. + +| Item | Status | Why | +|------|--------|-----| +| Add unsigned Android debug and iOS Simulator builds to pull-request CI | V1.1 backlog | The selected thorough V1 validates source and packaging contracts without the runner cost of complete platform builds; builds can be added after the stable check set is measured. | +| Add retained test artifacts, trend dashboards, or runner-cost reports | V1.1 backlog | V1 relies on concise Actions logs and summaries; historical reporting is useful only after the workflow set is stable. | +| Triage the 22 baseline Dependabot alerts reported when the dependency graph was enabled | V1.1 backlog | Dependency review blocks newly introduced vulnerable changes; the 16 high and six moderate alerts already on fork `main` require separate product-scoped ownership and dependency upgrades rather than an unreviewed monorepo rewrite. | +| Use private or self-hosted GitHub Actions runners | V1.1 backlog | Hosted runners avoid a new privileged machine and credential-maintenance boundary during cleanup. | +| Recreate official Ente deployment, release, app-store, container, translation, documentation-site, or cache-warming automation | Out of scope | Those workflows serve official Ente infrastructure and products rather than this private self-hosted mobile fork. | +| Preserve broad CI for Ensu, Auth, Locker, desktop Photos, web, server, Rust, documentation, or staff infrastructure | Out of scope | The selected problem framing protects the self-hosted Android and iOS Photos applications and their maintenance automation. | +| Put Firebase, Apple, signing, server, tester, Tailscale, Crowdin, Cloudflare, or deployment credentials in GitHub Actions | Out of scope | Private application distribution and operations remain guarded local owner workflows with secrets outside this public repository. | +| Automatically approve, merge, sign, publish, deploy, or synchronize source | Out of scope | Owner review and explicit local confirmations remain hard mutation boundaries. | + +**Status values:** + +- `V1.1 backlog` โ€” deferred but planned for the next milestone. +- `Out of scope` โ€” will not be done in this initiative; distinct from deferred + work. + +--- + +## 5. Decision log + +> Append-only. Newest entries stay on top. Never delete an entry; if a decision +> changes, add a newer entry explaining the reversal. + +### 2026-07-22 โ€” Protect main with five unique GitHub Actions gates + +**Decision:** Require `Actions CodeQL gate`, `Dependency review gate`, `Linux +mobile gate`, `macOS mobile gate`, and `Workflow security gate` from the GitHub +Actions app. Require a fresh branch and resolved conversations, enforce the +checks for administrators, and disable force pushes and branch deletion. + +**Why:** The repository had no branch protection. Unique app-bound names avoid +one generic `validate` result satisfying another workflow and turn the selected +fail-closed policy into an actual merge boundary. + +**Alternatives considered:** Leave green checks advisory, or require an +additional reviewer, which would prevent the sole owner from merging their own +validated maintenance PRs. + +### 2026-07-22 โ€” Keep required checks stable while filtering expensive work + +**Decision:** Trigger every retained pull-request workflow on every PR, return +a stable successful check for irrelevant changes, and perform path detection +inside jobs before Flutter, macOS, or CodeQL setup. Use explicit macOS and +workflow-security gate jobs to aggregate skipped/relevant validation safely. + +**Why:** GitHub leaves a path-filtered workflow pending when its check is +required but the workflow never starts. Stable checks make branch protection +possible without running expensive platform work for unrelated changes. + +**Alternatives considered:** Keep top-level path filters and leave checks +non-required, which does not fail closed, or require them anyway, which blocks +irrelevant pull requests indefinitely. + +### 2026-07-22 โ€” Treat the workflow set as a tested allowlist + +**Decision:** Approve exactly six workflow files and one local setup action. +Reject both additions and removals unless the trusted checker, its fixture +tests, and the allowlist change together under the workflow approval +environment. + +**Why:** Upstream synchronization can restore deleted workflows or supporting +actions without producing a merge conflict. Exact filenames, triggers, +permissions, runners, timeouts, environments, credentials, action pins, and +secret-free source turn that silent reintroduction into a blocking review +failure. + +**Alternatives considered:** Scan only whatever files happen to exist, which +cannot detect restored automation, or keep the unused deployment composite +action, which leaves executable but unowned publication code in the fork. + +### 2026-07-22 โ€” Enable dependency evidence and scan workflow code only + +**Decision:** Enable vulnerability alerts and the dependency graph for the +public fork, retain dependency review for the Photos mobile Pub/Rust boundary, +and reduce CodeQL to the `actions` language on workflow changes plus a weekly +backstop. + +**Why:** GitHub documents the dependency graph as the prerequisite for +dependency review, and enabling vulnerability alerts enabled that graph. Its +SBOM and comparison APIs now return valid data. GitHub Actions is the only +CodeQL language directly owned by this workflow-cleanup scope; repository-wide +Go and JavaScript scanning would restore unrelated monorepo cost. + +**Rollback:** `DELETE /repos/vanton1/ente/vulnerability-alerts` disables the +alerts and dependency graph. Deleting or reverting the two workflow edits +removes their checks without changing application or distribution state. + +**References:** [GitHub dependency review](https://docs.github.com/en/code-security/concepts/supply-chain-security/dependency-review), +[enabling the dependency graph](https://docs.github.com/en/enterprise-cloud@latest/code-security/how-tos/secure-your-supply-chain/secure-your-dependencies/enable-dependency-graph), +and [repository security endpoints](https://docs.github.com/en/rest/repos/repos). + +**Alternatives considered:** Leave dependency review broken, or keep broad +upstream CodeQL coverage for languages the fork does not maintain. + +### 2026-07-22 โ€” Keep maintenance mutations isolated from pull requests + +**Decision:** Keep upstream drift detection on schedule/manual triggers with +only `contents: read` and `issues: write`, and keep pull-request workflow +validation read-only behind its existing approval environment. Disable +persisted Git credentials for every checkout and require the exact fork name. + +**Why:** The drift reconciler legitimately mutates one tracking issue but never +runs pull-request code. The workflow checker does run pull-request source, so +it receives no write token and executes the trusted checker from the base +revision. + +**Alternatives considered:** Combine maintenance checks, which would mix write +authority with untrusted code, or remove the approval boundary, which would +weaken review of workflow changes. + +### 2026-07-22 โ€” Match CocoaPods to the checked-in lockfile + +**Decision:** Pin Ruby 3.3 and CocoaPods 1.17.0 in the macOS workflow, verify +that version before running, and check only the Photos iOS lockfile in +deployment mode. + +**Why:** The checked-in lockfile was generated by CocoaPods 1.17.0. CocoaPods +1.16.2 rejects it in deployment mode solely because it rewrites the generator +version, while 1.17.0 verifies and installs all 78 pods without a tracked diff. + +**Alternatives considered:** Use the runner's mutable preinstalled CocoaPods, +which reproduced the false failure, or verify all upstream apps, which exceeds +the fork's Photos boundary. + +### 2026-07-22 โ€” Split portable mobile checks from Apple-specific contracts + +**Decision:** Run endpoint behavior, Android release-tool contracts, Rust +binding generation, tracked-Dart formatting, and complete mobile analysis on +Linux. Reserve tests that invoke Apple tools and CocoaPods for the macOS lane. + +**Why:** The inherited Linux workflow mixed portable source validation with +tests that correctly require `plutil`. The replacement keeps broad source +quality while preserving the platform contract instead of weakening it. + +**Alternatives considered:** Skip all release-tool tests on Linux, which would +lose Android coverage, or mock Apple tooling on Linux, which would not prove +the actual iOS contract. + +### 2026-07-22 โ€” Remove 32 inherited workflows and replace four broad checks + +**Decision:** Remove 23 mutation-capable or scheduled workflows in the first +risk-reduction task and nine unrelated read-only checks after replacement CI +exists. Replace or adapt mobile lint, Podfile verification, dependency review, +and CodeQL; retain and harden only upstream drift and workflow security. + +**Why:** The complete 38-file audit shows that official releases, deployments, +translations, cache warming, products, and monorepo checks either fail for +expected infrastructure reasons or succeed without protecting this fork. +Six workflows map directly to the selected mobile, dependency, workflow, and +upstream-maintenance scope. + +**Alternatives considered:** Retain successful unrelated checks, which still +consume runners and enlarge the security surface; or disable files in GitHub +while keeping them in source, which hides rather than removes the ambiguity and +allows upstream edits to remain silently present. + +### 2026-07-22 โ€” Integrate workflow maintenance with current fork documentation + +**Decision:** Link this effort and its as-built companion to the fork overview, +self-hosted mobile index, upstream synchronization runbook and architecture, +workflow security contracts, and relevant build/test scripts. + +**Why:** GitHub Actions are part of the fork's maintenance and security model, +not an isolated subsystem. A future maintainer should reach the active policy +from the same entry points used for builds and upstream catch-up. + +**Alternatives considered:** A workflow-only document, which would be easier to +miss, and minimal standalone records, which would duplicate existing context +and weaken traceability. + +### 2026-07-22 โ€” Fail closed on every fork-owned check + +**Decision:** Treat retained mobile, dependency, workflow-security, and +allowlist failures as merge-blocking defects from V1, with least privilege, +SHA-pinned actions, and no operational secrets. + +**Why:** A green check must mean the protected behavior passed. Advisory or +ignored failures would recreate the noisy Actions surface this initiative is +removing. + +**Alternatives considered:** An advisory rollout, which permits regressions +during transition, and mixed enforcement, which protects workflows while +allowing mobile or dependency failures into `main`. + +### 2026-07-22 โ€” Produce an as-built GitHub Actions architecture companion + +**Decision:** End V1 with `ForkGitHubActionsArchitecture.md` describing the +settled workflow set, triggers, permissions, path filters, failures, rollback, +and future upstream adoption process. + +**Why:** Multiple workflows, repository settings, permission boundaries, and +an upstream-reintroduction guard interact. A settled companion will be more +useful than reconstructing the final system from task history. + +**Alternatives considered:** Keep architecture only in this living record, +which would mix historical implementation decisions with current maintenance +instructions. + +### 2026-07-22 โ€” Implement risk-first with pause-safe task boundaries + +**Decision:** Audit first; remove mutation-capable and scheduled inherited +automation before building the replacement; then harden security, prune the +remaining checks, enforce the allowlist, and run controlled live validation. + +**Why:** The inherited set is actively producing scheduled failures and +contains official deployment and release surfaces. Each task should leave a +reviewable state that can be reverted without operational side effects. + +**Alternatives considered:** Replacement-first, which keeps noisy and +potentially dangerous workflows active longer, and one atomic replacement, +which is harder to review, diagnose, and roll back. + +### 2026-07-22 โ€” Replace inherited automation with a fork-owned allowlist + +**Decision:** Delete irrelevant inherited workflow files, introduce focused +fork workflows, and test the exact allowed set so upstream synchronization +cannot silently reactivate automation. + +**Why:** Keeping or merely skipping dozens of official workflows leaves a +large security and maintenance surface and a confusing Actions page. Explicit +adoption makes each workflow intentional. + +**Alternatives considered:** Repair inherited core workflows in place, which +retains broad upstream assumptions, and guard every inherited workflow to the +official repository, which preserves clutter and skipped runs. + +### 2026-07-22 โ€” Deliver a thorough source-validation V1 without platform builds + +**Decision:** Retain upstream drift, workflow security, dependency security, +focused self-hosted mobile tests, formatting, full mobile analysis, and macOS +Podfile verification. Do not build signed or unsigned Android/iOS applications +in V1 CI. + +**Why:** This catches the fork's likely source and dependency regressions while +avoiding long, unrelated official builds and all signing/publication state. + +**Alternatives considered:** A narrow focused-test-only V1, which misses broad +Flutter or dependency regressions, and a strategic V1 with Android and iOS +builds, which adds substantial hosted-runner use before the core workflow set +is stable. + +### 2026-07-22 โ€” Frame CI around self-hosted Photos mobile reliability + +**Decision:** GitHub Actions in this fork serve the self-hosted Android and iOS +Ente Photos clients, their documentation and security boundaries, and guarded +upstream synchronization. + +**Why:** This is the fork's stated purpose. Most inherited failures come from +official Ente products, production infrastructure, and secrets the fork does +not own. + +**Alternatives considered:** Broad monorepo integrity, which consumes runners +and maintenance effort on unrelated systems, and upstream CI parity, which +would require recreating official Ente infrastructure and credentials. + +--- + +## 6. Open questions + +_None. Resolved decisions are recorded in ยง5._ + +--- + +## 7. Lessons learned + +> Populated at the end of each phase. Surprises, anti-patterns discovered, and +> things to do differently next time. + +### Phase 1 โ€” Audit permissions, triggers, services, and relevance together + +- A workflow with nominally read-only repository permissions can still deploy + through external credentials; permission, secret, environment, command, and + trigger evidence all matter. +- Successful inherited checks can be as noisy and costly as failed ones when + they protect unrelated products. Outcome alone is not a retention reason. +- Removing high-risk automation first leaves a much smaller review surface: + 15 workflow files remain for replacement, hardening, or deferred removal, + and none of the deleted files was needed to validate that reduced set. + +### Phase 2 โ€” Separate portable checks and authority boundaries + +- Platform contracts are clearest when Linux owns portable behavior and source + quality while macOS owns real Apple tooling and CocoaPods. +- A dependency lockfile is only deterministic when CI also pins the tool that + generated its metadata; CocoaPods 1.16.2 and 1.17.0 produced different + deployment results from identical source. +- Pull-request validation and issue mutation should remain separate workflows: + the former can stay read-only, while the latter never evaluates untrusted PR + code. + +### Phase 3 โ€” Make security scope explicit and machine-checkable + +- Enabling GitHub's dependency graph must be proven through the same comparison + API used by dependency review, not inferred from a settings response alone. +- Broad security scanning is not automatically better: Actions-only CodeQL and + Photos mobile dependency review provide evidence the fork can act on. +- An allowlist must reject missing files as well as unexpected ones; otherwise + a security workflow can be deleted while the checker still reports success. + +### Phase 4 โ€” Prove both sides of path-aware enforcement + +- Live relevant-path proof alone is incomplete. A disposable docs-only PR + showed that stable required checks remain green without allocating macOS or + running Flutter/CodeQL setup. +- Required check names must be unique and bound to their GitHub App; generic + `validate` and `Required gate` names are ambiguous in branch protection. +- GitHub repository settings are part of the architecture even though they are + not stored in Git. API readback and a merge-state check provide the handoff + evidence needed to keep them aligned with source. diff --git a/living_docs/ForkGitHubActionsArchitecture.md b/living_docs/ForkGitHubActionsArchitecture.md new file mode 100644 index 00000000000..e1fd5ac671f --- /dev/null +++ b/living_docs/ForkGitHubActionsArchitecture.md @@ -0,0 +1,202 @@ +# Fork GitHub Actions Architecture + +**Status:** Current as-built architecture +**Effective:** 2026-07-22 +**Repository:** `vanton1/ente` +**Implementation record:** [Fork GitHub Actions Maintenance](ForkGitHubActions.md) + +## Purpose and boundary + +GitHub Actions in this fork protect the configurable self-hosted Ente Photos +applications, dependency and workflow security, and guarded synchronization +with official `ente/ente`. They do not reproduce Ente's production CI. + +No allowed workflow signs or publishes an application, uploads to Firebase, +deploys a service or website, writes translations, publishes a container, +changes Museum or network state, registers an Apple device, or receives a +private operational secret. Android and iOS release operations remain guarded +local owner procedures. + +## Exact automation allowlist + +The trusted checker permits exactly these files: + +| Workflow | Trigger | Required pull-request check | Expensive work | Permissions | +|---|---|---|---|---| +| `self-hosted-mobile-linux.yml` | Every PR; manual | `Linux mobile gate` | Only for Photos mobile, relevant Rust, setup-action, or validator changes | `contents: read`, `pull-requests: read` | +| `self-hosted-mobile-macos.yml` | Every PR; manual | `macOS mobile gate` | macOS runner only for Photos/iOS dependency and validator changes | `contents: read`, `pull-requests: read` | +| `dependency-review.yml` | Every PR | `Dependency review gate` | Always invokes GitHub's dependency-diff API; normally completes in seconds | `contents: read` | +| `codeql.yml` | Every PR; Monday 01:22 UTC; manual | `Actions CodeQL gate` | PR analysis only when workflow or composite-action code changed; scheduled/manual analysis always runs | `contents: read`, `pull-requests: read`, `security-events: write` | +| `workflow-security-checks.yml` | Every PR | `Workflow security gate` | Trusted validation only when workflow, action, or checker policy changed | `contents: read`, `pull-requests: read` | +| `upstream-sync-drift.yml` | Daily 06:17 UTC; manual | Not a PR check | Always calculates official-upstream drift | `contents: read`, `issues: write` | + +The only allowed composite action is +`.github/actions/setup-flutter/action.yml`. It installs Flutter 3.38.10 from +the official release archive after verifying the platform-specific SHA-256. + +An upstream merge that restores another workflow or action fails the allowlist +until the owner explicitly reviews and adopts it. The historical disposition +of every inherited workflow is recorded in the +[implementation record](ForkGitHubActions.md#task-11-workflow-disposition-inventory). + +## Pull-request flow + +```text +pull request + | + +-- Linux gate -------- path API -- relevant? -- Flutter/Rust/test/analyze + | `------ no: stable green gate + +-- macOS gate -------- path API -- relevant? -- macOS iOS tests + Pods + | `------ no: skipped validation + green gate + +-- dependency gate --- GitHub dependency diff and advisory policy + +-- Actions CodeQL ---- path API -- relevant? -- Actions analysis + | `------ no: stable green gate + `-- workflow gate ----- path API -- relevant? -- approved trusted checker + `------ no: skipped validation + green gate +``` + +Path detection uses the GitHub pull-request files API and paginates all files. +It is performed before Flutter, Ruby/CocoaPods, macOS, or CodeQL setup. Manual +and scheduled runs treat their owned scope as relevant. Every PR workflow +starts even for irrelevant files, because a top-level `paths` filter can leave +a required check permanently pending when GitHub skips the whole workflow. + +The macOS and workflow-security workflows use a final `always()` gate. The gate +accepts a successful relevant validation or an intentional irrelevant skip, +but fails on path-detection, validation, cancellation, or ambiguous state. + +## Validation responsibilities + +### Linux mobile + +`scripts/test_self_hosted_mobile_linux.sh` restores the locked Flutter graph, +regenerates Flutter-Rust-Bridge bindings, rejects generated drift, and runs: + +- standard endpoint and Linux-portable Android release-tool contracts; +- configurable endpoint contracts with a public example HTTPS origin; +- locked endpoint contracts with the same safe example origin; +- tracked Dart formatting; and +- full mobile `flutter analyze`. + +No signing key, Firebase binding, app artifact, or real server address is used. + +### macOS mobile + +`scripts/test_self_hosted_mobile_macos.sh` requires macOS and exactly CocoaPods +1.17.0, matching the checked-in lockfile. The workflow pins Ruby 3.3, runs the +four iOS Ad Hoc/preparation/publication/identity contract suites, and executes +`pod install --deployment` only for Photos. It does not archive, sign, register, +or publish an IPA. + +### Dependency and code security + +Dependency review fails closed on vulnerable dependency changes in runtime, +development, or unknown scopes. GitHub vulnerability alerts and the dependency +graph are enabled for the public fork; the graph's SBOM and comparison API were +verified before making the check required. + +Enabling the graph reported 22 vulnerabilities already present on fork `main` +(16 high and six moderate). Dependency review prevents new vulnerable changes; +it does not retroactively repair that baseline. Product-scoped alert triage is +tracked as follow-up work rather than silently upgrading unrelated monorepo +dependencies in this cleanup. + +CodeQL scans only the `actions` language. Broad Go and JavaScript/TypeScript +analysis remains upstream-owned and is intentionally absent from this +self-hosted mobile boundary. + +### Workflow security + +`.github/scripts/check_workflow_security.rb` is loaded from the pull request's +base SHA by the approval-gated validation job and inspects the proposed tree. +It rejects: + +- missing or unexpected workflow/action files and job identities; +- privileged triggers or top-level PR path filters; +- permission, runner, timeout, fork-guard, environment, or stable-check drift; +- non-SHA external actions or unapproved local actions; +- checkout steps that persist credentials; and +- repository/environment secret references. + +Fixture tests prove both the accepted repository and representative failing +cases. `scripts/test_upstream_sync.sh` runs the checker and its tests alongside +the complete upstream-sync contract suite. + +### Upstream drift + +The drift workflow is the only allowed workflow with issue-write authority. It +runs no pull-request code, cannot push source, and reconciles one marker-based +tracking issue. See the [upstream synchronization architecture](UpstreamEnteSynchronizationArchitecture.md). + +## Main-branch enforcement + +The `main` branch is protected with: + +- strict, up-to-date required checks; +- the five exact gate names above, bound to the GitHub Actions app; +- enforcement for administrators; +- required conversation resolution; +- force pushes disabled; and +- branch deletion disabled. + +A separate approving reviewer is not required, so the owner can merge a clean +fork-maintenance PR. Direct pushes cannot bypass the required GitHub Actions +evidence. Branch protection can be inspected with: + +```sh +gh api repos/vanton1/ente/branches/main/protection +``` + +Repository settings are not stored in Git. If protection is deliberately +replaced, preserve the five exact check names and GitHub Actions app binding. +Removing protection or vulnerability alerts is an explicit owner rollback, +not a workflow operation. + +## Failure and recovery + +1. Open the failed gate and identify whether path detection, setup, validation, + or aggregation failed. +2. Reproduce portable checks with + `scripts/test_self_hosted_mobile_linux.sh` and the policy/sync suite with + `scripts/test_upstream_sync.sh`. +3. Reproduce the macOS lane only on a Mac with Flutter 3.38.10, Ruby 3.3, and + CocoaPods 1.17.0. +4. Repair source, generation, lockfiles, action pins, or policy explicitly. + Never add secrets or disable a gate to make a PR green. +5. Push the repair and wait for all five required checks on the new head SHA. + +Cancellation is fail-closed. Concurrency cancels superseded runs for the same +workflow/ref; branch protection evaluates the latest PR commit. + +## Adopting future upstream automation + +When official Ente adds or changes automation during synchronization: + +1. leave the new file blocked by the allowlist; +2. identify its product, trigger, permissions, secrets, services, mutations, + runner cost, and fork relevance; +3. prefer extending an existing fork workflow over importing official release + or deployment machinery; +4. pin every external action to a full commit SHA, remove secrets, add the exact + fork guard and timeout, and retain only minimal permissions; +5. update the checker rules and fixture tests in the same reviewed PR; +6. update this architecture if triggers, gates, settings, or authority change; + and +7. prove relevant and irrelevant PR behavior before accepting the change. + +Production Ente releases, deployments, translations, cache warming, unrelated +product CI, and signing/distribution credentials remain out of scope unless the +fork owner starts a new explicitly designed initiative. + +## Acceptance evidence + +The complete relevant-path set passed on +[PR #5](https://github.com/vanton1/ente/pull/5): workflow security in 11 seconds, +dependency review in 8 seconds, Actions CodeQL in 44 seconds, macOS mobile in +5m43s, and Linux mobile in 13m56s. All five stable gates were successful. + +A disposable Markdown-only [PR #6](https://github.com/vanton1/ente/pull/6) +proved irrelevant-path behavior: all five gates passed in 2โ€“5 seconds, +macOS/workflow validation was explicitly skipped, and Linux/CodeQL performed +only path detection. The probe PR was closed and both probe branches were +deleted without merging its file. diff --git a/mobile/apps/photos/SELF_HOSTED_DOCUMENTATION.md b/mobile/apps/photos/SELF_HOSTED_DOCUMENTATION.md index e33cf96adde..c6d07caac35 100644 --- a/mobile/apps/photos/SELF_HOSTED_DOCUMENTATION.md +++ b/mobile/apps/photos/SELF_HOSTED_DOCUMENTATION.md @@ -43,6 +43,7 @@ binding and account flow become active. | Understand Ad Hoc signing, immutable iOS preparation, Firebase evidence, and recovery boundaries | Maintainer or operator | [iOS distribution architecture](../../../living_docs/FirebaseIOSDistributionArchitecture.md) | | Run, pause, recover, or review an official Ente synchronization | Maintainer or operator | [Upstream synchronization runbook](../../../UPSTREAM_SYNC.md) | | Understand the synchronization state machine, permissions, provenance, and safety boundaries | Maintainer | [Upstream synchronization architecture](../../../living_docs/UpstreamEnteSynchronizationArchitecture.md) | +| Understand the fork workflow allowlist, checks, permissions, branch protection, and upstream adoption | Maintainer | [Fork GitHub Actions architecture](../../../living_docs/ForkGitHubActionsArchitecture.md) | The tester guide is the only current document intended to be sent directly to testers. The operator supplies the exact Firebase invitation, Tailscale access, @@ -59,6 +60,8 @@ Museum origin, web-app origin, and account instructions privately. behavior; they are not recurring release checklists. - The **upstream synchronization runbook** owns current drift, integration, validation, publication, and recovery commands. +- The **fork GitHub Actions architecture** owns the current workflow allowlist, + merge gates, permissions, path filtering, and automation adoption policy. - The **living documents** preserve project decisions and acceptance evidence. They are historical implementation records, not current runbooks. - The Photos [README](README.md) is a contributor entry point, not another diff --git a/mobile/apps/photos/test/scripts/publish_self_hosted_android_release_test.dart b/mobile/apps/photos/test/scripts/publish_self_hosted_android_release_test.dart index 019a86050b9..42254234a29 100644 --- a/mobile/apps/photos/test/scripts/publish_self_hosted_android_release_test.dart +++ b/mobile/apps/photos/test/scripts/publish_self_hosted_android_release_test.dart @@ -279,6 +279,7 @@ void main() { "ente-firebase-receipt-test-", ); try { + Process.runSync("chmod", ["0700", temporaryDirectory.path]); final receiptPath = p.join( temporaryDirectory.path, "release-2158.firebase-release.json", @@ -332,6 +333,7 @@ void main() { "ente-firebase-attempt-test-", ); try { + Process.runSync("chmod", ["0700", temporaryDirectory.path]); final path = writeFailedPublicationAttempt( temporaryDirectory.path, prepared: preparedRelease(), diff --git a/scripts/test_self_hosted_mobile_linux.sh b/scripts/test_self_hosted_mobile_linux.sh new file mode 100755 index 00000000000..d0d94f7f2d2 --- /dev/null +++ b/scripts/test_self_hosted_mobile_linux.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash + +set -euo pipefail + +readonly repo_root="$(git rev-parse --show-toplevel)" +readonly mobile_dir="$repo_root/mobile" +readonly photos_dir="$mobile_dir/apps/photos" +readonly rust_dir="$repo_root/rust" +readonly flutter_bin="${FLUTTER_BIN:-flutter}" +readonly dart_bin="${DART_BIN:-dart}" +readonly cargo_bin="${CARGO_BIN:-cargo}" +readonly public_endpoint="https://photos.example.com" + +readonly -a endpoint_tests=( + test/core/network/endpoint_policy_test.dart + test/core/network/endpoint_switcher_test.dart + test/ui/settings/developer_settings_lock_test.dart + test/ui/settings/server_settings_page_test.dart +) +readonly -a linux_release_tests=( + test/scripts/prepare_self_hosted_android_release_test.dart + test/scripts/publish_self_hosted_android_release_test.dart +) + +cd "$mobile_dir" +"$flutter_bin" pub get --enforce-lockfile + +cd "$rust_dir" +"$cargo_bin" codegen frb +git -C "$repo_root" diff --exit-code + +cd "$photos_dir" +"$flutter_bin" test --no-pub \ + "${endpoint_tests[@]}" \ + "${linux_release_tests[@]}" + +"$flutter_bin" test --no-pub \ + --dart-define=configurableEndpoint=true \ + --dart-define="endpoint=$public_endpoint" \ + "${endpoint_tests[@]}" + +"$flutter_bin" test --no-pub \ + --dart-define=lockedEndpoint=true \ + --dart-define="endpoint=$public_endpoint" \ + "${endpoint_tests[@]}" + +cd "$repo_root" +git ls-files -z -- "*.dart" | + xargs -0 -n 200 "$dart_bin" format \ + --output=none \ + --set-exit-if-changed +git diff --exit-code + +cd "$mobile_dir" +"$flutter_bin" analyze --no-pub + +cd "$repo_root" +git diff --check +echo "Self-hosted mobile Linux validation passed." diff --git a/scripts/test_self_hosted_mobile_macos.sh b/scripts/test_self_hosted_mobile_macos.sh new file mode 100755 index 00000000000..d942781fb52 --- /dev/null +++ b/scripts/test_self_hosted_mobile_macos.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash + +set -euo pipefail + +readonly repo_root="$(git rev-parse --show-toplevel)" +readonly mobile_dir="$repo_root/mobile" +readonly photos_dir="$mobile_dir/apps/photos" +readonly flutter_bin="${FLUTTER_BIN:-flutter}" +readonly pod_bin="${POD_BIN:-pod}" +readonly expected_pod_version="1.17.0" + +if [[ "$(uname -s)" != "Darwin" ]]; then + echo "Self-hosted iOS validation requires macOS." >&2 + exit 1 +fi + +actual_pod_version="$($pod_bin --version)" +if [[ "$actual_pod_version" != "$expected_pod_version" ]]; then + echo "Expected CocoaPods $expected_pod_version, found $actual_pod_version." >&2 + exit 1 +fi + +cd "$mobile_dir" +"$flutter_bin" pub get --enforce-lockfile + +cd "$photos_dir" +"$flutter_bin" test --no-pub \ + test/scripts/build_self_hosted_ios_adhoc_test.dart \ + test/scripts/prepare_self_hosted_ios_release_test.dart \ + test/scripts/publish_self_hosted_ios_release_test.dart \ + test/scripts/self_hosted_ios_identity_test.dart + +cd "$photos_dir/ios" +"$pod_bin" install --deployment + +cd "$repo_root" +git diff --exit-code +git diff --check +echo "Self-hosted mobile macOS validation passed." diff --git a/scripts/test_upstream_sync.sh b/scripts/test_upstream_sync.sh index 814d4844296..b5f0e3c9567 100755 --- a/scripts/test_upstream_sync.sh +++ b/scripts/test_upstream_sync.sh @@ -10,12 +10,15 @@ ruby -w -c scripts/upstream_sync.rb ruby -w -c scripts/test/upstream_sync_test.rb ruby -w -c scripts/test/upstream_sync_integration_test.rb ruby -w -c scripts/test/upstream_sync_workflow_test.rb +ruby -w -c .github/scripts/check_workflow_security.rb +ruby -w -c .github/scripts/check_workflow_security_test.rb ruby scripts/test/upstream_sync_test.rb ruby scripts/test/upstream_sync_integration_test.rb ruby scripts/test/upstream_sync_workflow_test.rb node --test .github/scripts/upstream-sync-issue.test.cjs ruby .github/scripts/check_workflow_security.rb +ruby .github/scripts/check_workflow_security_test.rb git diff --check