#!/bin/sh
# Single source of truth for "what ships inside the XPI", plus the version
# derived from it.
#
# Three consumers used to hand-maintain their own copy of this list, and
# keeping three copies of one fact in sync by hand is how issue #2397 happened:
#
#   1. web-ext's --ignore-files      (extension/package.json's four scripts)
#   2. the :(exclude) pathspec       (what moves the version — a WIDER
#                                     set than the ignore list; see
#                                     NOT_VERSION_RELEVANT)
#   3. the git-log pathspec          (the derived version, below)
#
# They now all read from here. POSIX sh only — CI's run shell is busybox.
#
# -f (no pathname expansion) is set for the whole script and is load-bearing:
# the lists below are iterated with deliberate word-splitting, and without -f
# the shell would also GLOB them, expanding `test/**` into whatever files
# happen to exist and corrupting the output. A caller's own `set -f` does not
# help here — this runs as a separate sh process and does not inherit it.
# Callers still need their own `set -f` for the substituted result; the two
# guards protect different expansions.
set -euf

# Paths under extension/ that are NOT packaged into the XPI.
#
# Split by whether git tracks them: node_modules and web-ext-artifacts are
# build/dependency output that never appears in a commit, so they belong in
# web-ext's ignore list but would be meaningless in a git pathspec.
#
# Directories need BOTH forms. `test/**` matches the files inside, but not the
# directory entry itself — web-ext writes an entry for the directory too, so
# with only the glob the XPI ends up carrying empty `test/` and `scripts/`
# entries (caught by the XPI-content check on 2026-08-03). The bare name alone
# is not enough either: minimatch's `test` does not match `test/url.spec.js`,
# so dropping the glob would ship the contents. Keep both.
NOT_PACKAGED_TRACKED='package.json package-lock.json README.md .gitignore vitest.config.js scripts scripts/** test test/**'
NOT_PACKAGED_BUILD='web-ext-artifacts node_modules'

# Paths under extension/ that cannot change the SHIPPED BYTES, and so must not
# move the derived version.
#
# Deliberately NOT the same list as NOT_PACKAGED_TRACKED, and the whole
# difference is `scripts/`. packaging.sh is not packaged into the XPI — but it
# DECIDES the version string, and build.yml stamps that string into the
# manifest.json that is packaged. A change to how the version is computed is
# therefore a change to the shipped bytes.
#
# Excluding it was harmless only while every push rebuilt the web image.
# Milestone 313 step 4 made the rebuild conditional on the derived revision
# moving, which turned it into a silent failure: a packaging.sh change gives a
# NEW version, so sign-extension misses its ext-<version> cache and signs —
# while build-web sees an unmoved revision, reuses the published image, and
# ships the OLD XPI. An orphaned AMO signature, and an instance quietly serving
# code the registry says is current.
#
# The two directions are not symmetric, which is why this list is the narrower
# one. Too wide costs a re-sign and a rebuild for a change that ships nothing
# new. Too narrow serves stale bytes and says nothing.
NOT_VERSION_RELEVANT='package.json package-lock.json README.md .gitignore vitest.config.js test test/**'

usage() {
  echo "usage: packaging.sh {ignore|pathspec|version|major-minor|patch}" >&2
  exit 2
}

# web-ext --ignore-files values, space-separated.
#
# Callers MUST disable pathname expansion first (`set -f`), or the shell will
# glob `test/**` against the working tree before web-ext ever sees the pattern
# and silently narrow it to whatever happens to exist right now.
cmd_ignore() {
  echo "$NOT_PACKAGED_TRACKED $NOT_PACKAGED_BUILD"
}

# git pathspec excluding the tracked files that cannot change the shipped
# bytes, e.g. :(exclude)extension/package.json :(exclude)extension/test/**
#
# This answers "what moves the version?", NOT "what goes in the XPI?" — see
# NOT_VERSION_RELEVANT for why those differ. cmd_ignore answers the other one.
# Same `set -f` requirement as above.
cmd_pathspec() {
  for entry in $NOT_VERSION_RELEVANT; do
    printf ':(exclude)extension/%s ' "$entry"
  done
  echo
}

# MAJOR.MINOR stays hand-set in manifest.json — it's the part that carries
# deliberate meaning. Only the patch component is derived.
cmd_major_minor() {
  root=$(git rev-parse --show-toplevel)
  grep -E '"version"' "$root/extension/manifest.json" \
    | head -1 \
    | sed -E 's/.*"version"[[:space:]]*:[[:space:]]*"([0-9]+)\.([0-9]+).*/\1.\2/'
}

# 2020-01-01T00:00:00Z — the anchor for the derived patch component. Fixed
# forever; moving it would renumber every version downwards.
VERSION_EPOCH=1577836800

# Minutes since VERSION_EPOCH of the LATEST commit that touched a PACKAGED
# extension file.
#
# Time-derived, per family rule 149: an artifact's ordering key must never be a
# commit count. A count is per-branch — `dev` and `main` count different
# histories of the same code — so the moment BOTH channels publish, their
# versions order by which branch accumulated more commits rather than by which
# is newer. A squash-merge makes that permanent: main gains one commit where dev
# gained five, so dev climbs away from main and a dev install can never cross
# back. That is Roundtable's 2026-08-24 incident (`versionCode` was the branch's
# commit count) in a different repo. Measured here on 2026-08-27: main=23,
# dev=24 under the old formula — one apart, which is exactly how the inversion
# stays invisible until it strands somebody.
#
# Why the commit's time and not the build's:
#   * MONOTONIC — max() over a set that only ever gains members. Verified
#     across all 24 extension-touching commits: zero non-monotonic steps.
#   * STABLE while the extension is unchanged, so an unchanged extension keeps
#     its version, the ext-<version> signature cache still hits, and AMO is
#     called once per extension CHANGE rather than once per push. Build-time
#     minutes would re-sign on every push and never let two channels share a
#     signature.
#   * SHARED ACROSS CHANNELS — after a merge, `main` sees the same commit and
#     derives the same number, so `:latest` reuses the signature `:dev` already
#     produced for byte-identical code. Same code, same version, one signing.
#   * REPRODUCIBLE — any checkout of a commit yields that commit's version.
#
# Requires real history: a depth-1 clone sees one commit and will derive a wrong
# (too low) value. Every consumer must check out with fetch-depth: 0.
cmd_patch() {
  root=$(git rev-parse --show-toplevel)
  # Unquoted on purpose: the pathspec must word-split into separate args.
  # Globbing is already off script-wide (set -euf above).
  # shellcheck disable=SC2046
  ts=$(cd "$root" && git log --format=%ct HEAD -- extension/ $(cmd_pathspec) \
       | sort -n | tail -1)
  if [ -z "$ts" ]; then
    echo "packaging.sh: no commit touches a packaged extension file" >&2
    exit 1
  fi
  echo $(( (ts - VERSION_EPOCH) / 60 ))
}

cmd_version() {
  echo "$(cmd_major_minor).$(cmd_patch)"
}

[ $# -ge 1 ] || usage
case "$1" in
  ignore)      cmd_ignore ;;
  pathspec)    cmd_pathspec ;;
  version)     cmd_version ;;
  major-minor) cmd_major_minor ;;
  patch)       cmd_patch ;;
  *)           usage ;;
esac
