From 53fbf8a994290cdbee1a6c2613e8f97f8bc2e000 Mon Sep 17 00:00:00 2001 From: Mobsuccess Bot Date: Tue, 4 Aug 2026 07:57:46 +0000 Subject: [PATCH] chore(gha): update mobsuccess.yml GitHub workflow --- .github/workflows/mobsuccess.yml | 132 ++++++++++++++++++++++++++++++- 1 file changed, 131 insertions(+), 1 deletion(-) diff --git a/.github/workflows/mobsuccess.yml b/.github/workflows/mobsuccess.yml index 3d2766e..d809ddc 100644 --- a/.github/workflows/mobsuccess.yml +++ b/.github/workflows/mobsuccess.yml @@ -23,7 +23,7 @@ on: jobs: MobsuccessPRCompliance: name: "Mobsuccess PR Compliance" - runs-on: ubuntu-24.04 + runs-on: ubuntu-slim timeout-minutes: 2 steps: - uses: mobsuccess-devops/github-actions-mobsuccess@master @@ -32,4 +32,134 @@ jobs: amplify-uri: ${{ secrets.AWS_AMPLIFY_URI }}${{ vars.AWS_AMPLIFY_URI }} storybook-amplify-uri: ${{ secrets.AWS_STORYBOOK_AMPLIFY_URI }}${{ vars.AWS_STORYBOOK_AMPLIFY_URI }} action: "validate-pr" + + # blob:none keeps this cheap even on the monolith: full commit graph (needed + # for the base...head merge base), file contents fetched only on demand. + - name: Checkout + if: github.event_name == 'pull_request' + uses: actions/checkout@v6 + with: + fetch-depth: 0 + filter: blob:none + + # A .gitignore pattern that only reaches INSIDE a directory (dir/, dir/*, + # dir/**) does not ignore a symlink or file at `dir` itself, so `git add -A` + # stages it. Merging that commit makes git delete the ignored directory to + # put the tracked entry in its place — silently, with no trash and no reflog + # for untracked content. Already happened in the org: panoramai#433. + - name: Ignored directory replacement + if: github.event_name == 'pull_request' + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + set -uo pipefail + fail=0 + + # Fail closed. This guard stands in front of an irreversible deletion, so + # it must never report success because git itself failed: an empty result + # from a command that errored would otherwise read as "nothing to flag". + # The runner's default shell is `bash -e`, but nothing here leans on that: + # check-ignore's non-zero exit is this script's core predicate, so every + # git call whose silence would be mistaken for success is checked by hand. + # + # printf rather than echo throughout: some shells expand backslash escapes + # in echo, which would turn a literal \n inside a path back into a real + # newline and undo the escaping below. + die() { printf '%s\n' "::error::$(esc "$1")"; exit 1; } + + # GitHub parses workflow commands one line at a time, so every value + # interpolated into one is escaped: a path may legally contain a newline, + # which would otherwise let it forge a second command. Property values + # need `:` and `,` too, as those delimit the property list. + esc() { local s=${1//'%'/%25}; s=${s//$'\r'/%0D}; s=${s//$'\n'/%0A}; printf '%s' "$s"; } + esc_prop() { local s; s=$(esc "$1"); s=${s//:/%3A}; s=${s//,/%2C}; printf '%s' "$s"; } + + work=$(mktemp -d) + trap 'rm -rf "$work"' EXIT + + # -z keeps paths literal. Without it git C-quotes any path containing a + # control character, and that quoted string matches neither the repo nor + # the ignore rules — so a crafted path could slip past the check. Literal + # paths are why the escaping above is required, not optional. + git diff --raw -z --no-renames --diff-filter=AMT "$BASE_SHA...$HEAD_SHA" > "$work/diff" \ + || die "Cannot diff $BASE_SHA...$HEAD_SHA. Refusing to pass without having checked." + git ls-tree -r -z --name-only "$HEAD_SHA" > "$work/tree" \ + || die "Cannot list the tree at $HEAD_SHA. Refusing to pass without having checked." + + # Probe sandbox: HEAD's .gitignore files and nothing else. Probing cannot + # happen in the real worktree — once the symlink exists on disk, git + # refuses to resolve a pathspec "beyond a symbolic link". + sandbox="$work/sandbox" + mkdir -p "$sandbox" + git -C "$sandbox" init -q . + + # Only ignore rules committed to the repo may decide the verdict. A + # runner's global core.excludesFile and the info/exclude the init + # template writes both reach --no-index, and would make the result + # depend on the machine rather than on the PR. + : > "$sandbox/.git/info/exclude" + ignored() { + git -c core.excludesFile=/dev/null -C "$sandbox" \ + check-ignore -q --no-index -- "$1" + } + + while IFS= read -r -d '' gi; do + case "$gi" in .gitignore|*/.gitignore) ;; *) continue ;; esac + mkdir -p "$sandbox/$(dirname "$gi")" + git show "$HEAD_SHA:$gi" > "$sandbox/$gi" \ + || die "Cannot read $gi at $HEAD_SHA. Refusing to pass without having checked." + done < "$work/tree" + + # --raw -z emits ": " and the path as two separate + # NUL-terminated records, so read them in pairs. + while IFS= read -r -d '' meta && IFS= read -r -d '' path; do + mode=$(printf '%s' "$meta" | awk '{print $2}') + # A tracked directory is normal; only blobs and symlinks land at a path. + case "$mode" in 040000|000000) continue ;; esac + + # Hole test, decided by git's own matcher rather than pattern parsing, + # so negations, nesting and precedence are handled for free: children + # of the path are ignored, the path itself is not. check-ignore uses + # lstat, so a symlink to a real directory still reads as "not a dir". + if ! ignored "$path" && ignored "$path/__probe__"; then + kind="file" + if [ "$mode" = "120000" ]; then + t=$(git show "$HEAD_SHA:$path") \ + || die "Cannot read the symlink target of $path at $HEAD_SHA." + kind="symlink -> $t" + fi + printf '%s\n' "::error file=$(esc_prop "$path")::$(esc "Tracking '$path' ($kind) will DELETE the ignored directory of the same name when this is merged or checked out — its contents are gitignored but the path itself is not.")" + fail=1 + fi + + # Independent rule: a tracked symlink must not escape the worktree. + if [ "$mode" = "120000" ]; then + target=$(git show "$HEAD_SHA:$path") \ + || die "Cannot read the symlink target of $path at $HEAD_SHA." + # normpath is pure string work: it must not resolve against the + # worktree, where the symlink already exists. + resolved=$(python3 -c 'import posixpath,sys; print(posixpath.normpath(posixpath.join(posixpath.dirname(sys.argv[1]), sys.argv[2])))' "$path" "$target") \ + || die "Cannot resolve the symlink target of $path." + escape=0 + case "$target" in /*) escape=1 ;; esac + case "$resolved" in ..*) escape=1 ;; esac + if [ "$escape" -eq 1 ]; then + printf '%s\n' "::error file=$(esc_prop "$path")::$(esc "Symlink '$path' -> '$target' points outside the repository. Tracked symlinks must stay repo-internal.")" + fail=1 + fi + fi + done < "$work/diff" + + if [ "$fail" -ne 0 ]; then + echo "" + echo "Remove the offending entry from the commit: git rm --cached " + echo "" + echo "Do NOT 'fix' the .gitignore instead: 'dir/*' plus '!dir/keep' cannot be" + echo "rewritten as a bare 'dir', because git will not re-include anything inside" + echo "an ignored directory. A bare 'dir' is correct only where there is no" + echo "negation to preserve." + exit 1 + fi + echo "No ignored-directory replacement detected." # DO NOT EDIT: END