Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
96 commits
Select commit Hold shift + click to select a range
790c8cc
fix(postmerge): carry verified embed safety changes site/src/componen…
appdevelopsk Nov 6, 2026
4ee8db3
fix(postmerge): carry verified embed safety changes site/src/lib/tool…
appdevelopsk Nov 6, 2026
b78cecc
fix(postmerge): carry verified embed safety changes site/src/lib/tool…
appdevelopsk Nov 6, 2026
f4e870b
fix(postmerge): carry verified embed safety changes site/scripts/grow…
appdevelopsk Nov 6, 2026
e878b1f
docs(postmerge): isolate unmerged PR40 follow-up and correct release …
appdevelopsk Nov 6, 2026
fa3b7eb
test(postmerge): add PR41-specific no-Actions full release gate with …
appdevelopsk Nov 6, 2026
56f4286
docs(postmerge): document branch-guarded PR41 release verification
appdevelopsk Nov 6, 2026
d742daa
fix(embed): block javascript/data/relative URLs in generated iframe s…
appdevelopsk Nov 6, 2026
5350c20
test(integration): assert safe URL scheme validation on embedded snip…
appdevelopsk Nov 6, 2026
2c29c1b
test(embed): cover malicious URL schemes and legitimate HTTPS/local s…
appdevelopsk Nov 6, 2026
8167090
docs(postmerge): record scheme-safe embed URLs and 4 passing standalo…
appdevelopsk Nov 6, 2026
d821db5
docs(postmerge): record actually executed 5/5 isolated Node embed tests
appdevelopsk Nov 6, 2026
ae03113
fix(embed): reject embedded URL credentials and control characters
appdevelopsk Nov 7, 2026
381095e
fix(embed): correct control-character regex escaping
appdevelopsk Nov 7, 2026
aa7c5e3
test(embed): reject credential-bearing and control-character URLs
appdevelopsk Nov 7, 2026
a5ec82b
test(embed): use actual escaped linebreak and tab in URL validation
appdevelopsk Nov 7, 2026
aad3e47
test(integration): preserve URL credential and whitespace guards
appdevelopsk Nov 7, 2026
587be7d
docs(postmerge): record URL parser edge case, mitigation and standalo…
appdevelopsk Nov 7, 2026
780404b
safety: prevent GitHub Actions auto-deploy on post-merge main push
appdevelopsk Nov 7, 2026
b387597
safety: require fresh main ancestry and manual deploy trigger in PR41…
appdevelopsk Nov 7, 2026
4432742
test(embed): execute exact TypeScript helper with dependency-free Nod…
appdevelopsk Nov 7, 2026
b762f9f
test: keep PR41 fresh-main and manual-deployment checks as release pr…
appdevelopsk Nov 7, 2026
c307366
test(release): run dependency-free real TS embed regression before np…
appdevelopsk Nov 7, 2026
50705f2
fix: make main auto-deploy release guard actually match workflow trigger
appdevelopsk Nov 7, 2026
de540f2
test: execute exact grep-based deploy-trigger guard against enabled a…
appdevelopsk Nov 7, 2026
111e5e1
fix(release): correctly reject push-triggered VPS deploy workflow
appdevelopsk Nov 7, 2026
718ca39
test: use real newlines in main-push deploy guard regression fixture
appdevelopsk Nov 7, 2026
9491038
docs(postmerge): record hash-verified tests and fixed automatic deplo…
appdevelopsk Nov 7, 2026
888f926
test: align PR41 regression with push-key deploy guard and exercise Y…
appdevelopsk Nov 7, 2026
6cf92c4
test: recognize any indented YAML push trigger without fragile regex …
appdevelopsk Nov 7, 2026
58cbfa2
docs: record exact-blob Node tests, four release preflight outcomes a…
appdevelopsk Nov 7, 2026
64dc50e
security: validate that deploy workflow contains only manual dispatch…
appdevelopsk Nov 7, 2026
ab2e2a0
test: exercise manual-only deploy workflow validator against YAML tri…
appdevelopsk Nov 7, 2026
2f88b7f
safety: gate PR41 on manual-only YAML event validation and executable…
appdevelopsk Nov 7, 2026
e1376a3
fix(release): fail closed unless deploy workflow has manual dispatch …
appdevelopsk Nov 7, 2026
ba61f6e
test(release): cover push schedule workflow-run and malformed deploy …
appdevelopsk Nov 7, 2026
0286579
test: use strict manual-only event validator in PR41 integration gate…
appdevelopsk Nov 7, 2026
5dd4217
chore(release): remove duplicate manual-deploy checker; retain verifi…
appdevelopsk Nov 7, 2026
1e47df1
chore(release): remove duplicate manual-deploy checker; retain verifi…
appdevelopsk Nov 7, 2026
02f649e
docs: record exact-hash Node execution of fail-closed manual-only wor…
appdevelopsk Nov 7, 2026
2866445
docs(postmerge): log 7/7 live-source assertions and hash-verified 4/4…
appdevelopsk Nov 7, 2026
52e2389
docs: append executed manual-only deploy parser and release-gate evid…
appdevelopsk Nov 7, 2026
ab650f2
fix: classify Web Share API cancellation without clipboard side effects
appdevelopsk Nov 7, 2026
ceadf3b
fix: do not copy URL after user cancels native share sheet
appdevelopsk Nov 7, 2026
ff3b3fa
test: verify Web Share API cancel vs genuine failure
appdevelopsk Nov 7, 2026
98ecfa4
test: verify Web Share API cancel vs genuine failure
appdevelopsk Nov 7, 2026
e8399ca
test: run share-cancel regression before npm install in PR41 release …
appdevelopsk Nov 7, 2026
368c3df
test: guard native-share cancellation against implicit clipboard fall…
appdevelopsk Nov 7, 2026
a3cc103
fix(release): pin canonical production origin for PR41 full build
appdevelopsk Nov 7, 2026
383f09c
docs(postmerge): record production-domain build guard and live free s…
appdevelopsk Nov 7, 2026
12f2e24
fix: resync shared-result button on locale and tool navigation
appdevelopsk Nov 7, 2026
d671f57
test: guard ShareBar route-state sync and native share cancellation h…
appdevelopsk Nov 7, 2026
1a325f9
docs: log actual 8/8 Web Share/embed tests, navigation guard and publ…
appdevelopsk Nov 7, 2026
a002a7a
safety: make Toolify CI workflow manual-only while Actions unavailable
appdevelopsk Nov 7, 2026
525847f
safety: disable automatic PR/push live index workflow executions
appdevelopsk Nov 7, 2026
15a4cd0
safety: inspect every GitHub Actions workflow rather than deploy.yml …
appdevelopsk Nov 7, 2026
cdfe02d
safety: gate release on all GitHub workflow triggers
appdevelopsk Nov 7, 2026
0ffd892
test(release): validate all GitHub workflows are manual-only using ex…
appdevelopsk Nov 7, 2026
a2c05d2
test: fail release if any CI or other workflow reintroduces automatic…
appdevelopsk Nov 7, 2026
9b07e52
test(release): require manual events in every GitHub workflow
appdevelopsk Nov 7, 2026
7a7514e
test: integrate all-workflow trigger validation into PR41 release checks
appdevelopsk Nov 7, 2026
9fe4321
test(release): gate all Actions workflows against unintended auto-runs
appdevelopsk Nov 7, 2026
c8f09f9
fix(release): recognize .yml/.yaml files in all-workflow manual-only …
appdevelopsk Nov 7, 2026
f1e6634
fix(test): use actual YAML newlines and valid filename regex in workf…
appdevelopsk Nov 7, 2026
62f6627
test: correct directory workflow fixtures and error-path patterns
appdevelopsk Nov 7, 2026
ea37d8d
chore(release): consolidate all-workflow gate around existing checker
appdevelopsk Nov 7, 2026
0ec6814
chore(release): remove redundant all-workflow checker after integrati…
appdevelopsk Nov 7, 2026
40efc62
chore(release): remove redundant all-workflow checker after integrati…
appdevelopsk Nov 7, 2026
502220d
docs(release): record all 8 manual workflows and fix directory regres…
appdevelopsk Nov 7, 2026
6f34056
docs: record 8-workflow manual-only guard and executed 6/6 Node tests
appdevelopsk Nov 7, 2026
d30a608
safety: fail closed on stale VPS deploy lock instead of non-atomic ta…
appdevelopsk Nov 7, 2026
3230b93
fix: require writable lock metadata, portable timestamp and verified …
appdevelopsk Nov 7, 2026
c640e55
test: verify deploy lock exclusivity, stale refusal and concurrent ac…
appdevelopsk Nov 7, 2026
798f0d0
test: run lock exclusivity and stale-refusal regression in release gate
appdevelopsk Nov 7, 2026
265a947
fix(seo): detect robots none directive as noindex in live growth smoke
appdevelopsk Nov 7, 2026
960f6cf
test(seo): cover robots none and guard against directive prefix false…
appdevelopsk Nov 7, 2026
b9fb6eb
fix(seo): reject unexpected canonical and redirect query/fragment par…
appdevelopsk Nov 7, 2026
4bdee09
docs: record exact-hash deploy lock concurrency tests and manual stal…
appdevelopsk Nov 7, 2026
609e0c3
fix(seo): compare exact sitemap loc URLs instead of prefix search
appdevelopsk Nov 7, 2026
bd0ff4e
test(seo): enforce exact sitemap location and reject misleading prefixes
appdevelopsk Nov 7, 2026
1b6ae67
fix(seo): use exact sitemap entries for live growth smoke
appdevelopsk Nov 7, 2026
d3dffcf
docs: record repeated 12-worker lock test runs and accurate stale thr…
appdevelopsk Nov 7, 2026
b19523e
fix(test): malformed sitemap loc is the negative case, not a valid URL
appdevelopsk Nov 7, 2026
b26d347
docs(release): record exact-blob 4/4 SEO smoke and offline concurrent…
appdevelopsk Nov 7, 2026
42bc78c
fix(seo): qualify distributed embed attribution links as nofollow
appdevelopsk Nov 7, 2026
6039cf0
docs(embed): describe attribution as user benefit, not forced SEO bac…
appdevelopsk Nov 7, 2026
8cbc09f
fix: reject fake data-* HTML attributes in live SEO and software CTA …
appdevelopsk Nov 7, 2026
352e4c5
test(seo): require nofollow on embedded widget attribution
appdevelopsk Nov 7, 2026
39b230d
test: reject data-* and quoted lookalike HTML attributes in growth smoke
appdevelopsk Nov 7, 2026
f0ea009
test(embed): keep visible attribution while qualifying distributed wi…
appdevelopsk Nov 7, 2026
584ae72
fix(test): use word boundary for actual HTML anchor matching
appdevelopsk Nov 7, 2026
168e9e4
test(seo): verify safe visible nofollow attribution in actual embed HTML
appdevelopsk Nov 7, 2026
3666808
fix: ignore commented and CDATA-only loc entries in sitemap audits
appdevelopsk Nov 7, 2026
2e07ce7
test: sitemap comment and CDATA must not pass live indexability check
appdevelopsk Nov 7, 2026
cef1aff
docs(pr41): record Google widget-link spam mitigation and 7/7 verifie…
appdevelopsk Nov 7, 2026
f2cd481
docs: log exact-source 5/5 live-growth smoke tests and false-positive…
appdevelopsk Nov 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
name: CI

# CI remains available for future explicit owner action, not automated PR/push runs.
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch: {}

jobs:
validate-and-build:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
name: Deploy to VPS

# The owner performs production deployments manually. Keep the workflow available
# for an explicit future decision, but never auto-deploy from a main push.
on:
push:
branches: [main]
workflow_dispatch: {}

concurrency:
Expand Down
13 changes: 2 additions & 11 deletions .github/workflows/toolify-live-index.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,7 @@
name: Toolify live indexability recovery (12 URLs)
# Keep recovery inspection opt-in while GitHub Actions are unavailable.
on:
pull_request:
paths:
- 'scripts/toolify-live-index-gate.mjs'
- 'scripts/tests/toolify-live-index-gate.test.mjs'
- '.github/workflows/toolify-live-index.yml'
push:
branches: [main]
paths:
- 'scripts/toolify-live-index-gate.mjs'
- '.github/workflows/toolify-live-index.yml'
workflow_dispatch:
workflow_dispatch: {}
jobs:
source:
runs-on: ubuntu-latest
Expand Down
147 changes: 147 additions & 0 deletions docs/PR40_POSTMERGE_FOLLOWUP_2026-11-07.md

Large diffs are not rendered by default.

25 changes: 18 additions & 7 deletions ops/deploy-lock.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,31 +17,42 @@
set -u
cmd=${1:-}; L=${2:-}; me=${3:-}; stale=${4:-3600}
[[ -n $cmd && -n $L && -n $me ]] || { echo "usage: acquire|release <lockdir> <owner> [stale_sec]"; exit 2; }
[[ $stale =~ ^[0-9]+$ ]] || { echo "stale_sec must be a non-negative integer" >&2; exit 2; }
[[ $L == */.deploy.lock.d ]] && { echo "lockdir must be outside the deploy dir: $L"; exit 2; }
now=$(date +%s)
case $cmd in
acquire)
if mkdir "$L" 2>/dev/null; then
echo "$me $now" > "$L/owner"
if ! printf '%s %s\n' "$me" "$now" > "$L/owner"; then
rmdir "$L" 2>/dev/null || true
echo "Cannot write lock owner metadata: $L/owner" >&2
exit 1
fi
echo "ACQUIRED $L by $me"
exit 0
fi
o=$(cut -d' ' -f1 "$L/owner" 2>/dev/null || true)
t=$(stat -c %Y "$L/owner" 2>/dev/null || stat -c %Y "$L" 2>/dev/null || echo "$now")
t=$(stat -c %Y "$L/owner" 2>/dev/null || stat -f %m "$L/owner" 2>/dev/null || stat -c %Y "$L" 2>/dev/null || stat -f %m "$L" 2>/dev/null || echo "$now")
age=$(( now - t ))
# A deploy that died without releasing must not block forever.
# Do not take over an old lock automatically. The previous code overwrote
# the owner file without any atomic claim; two waiters could both enter a
# live deployment (or steal a lock from a slow, still-running deploy).
# A stale lock requires verification that the prior deployment has stopped.
if (( age > stale )); then
echo "$me $now" > "$L/owner"
echo "TAKEOVER stale lock of ${o:-unknown} (age ${age}s > ${stale}s) by $me"
exit 0
echo "STALE $L held by ${o:-unknown} for ${age}s; refusing unsafe automatic takeover. Verify the prior deploy is inactive before manually clearing the lock." >&2
exit 3
fi
echo "BUSY $L held by ${o:-unknown} for ${age}s"
exit 3
;;
release)
o=$(cut -d' ' -f1 "$L/owner" 2>/dev/null || true)
if [[ $o == "$me" ]]; then
rm -f "$L/owner" && rmdir "$L" && echo "RELEASED $L by $me"
if ! { rm -f "$L/owner" && rmdir "$L"; }; then
echo "Failed to release lock: $L" >&2
exit 1
fi
echo "RELEASED $L by $me"
else
echo "NOT OWNER (held by ${o:-nobody}); left as is"
fi
Expand Down
66 changes: 66 additions & 0 deletions ops/tests/deploy-lock.test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#!/usr/bin/env bash
# Offline, non-production regression tests for the shared VPS deploy lock.
# No remote SSH, GitHub Actions, application deployment or network access.
set -Eeuo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
HELPER="$ROOT/ops/deploy-lock.sh"
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
LOCK="$TMP/toolify.deploy.lock.d"
OUT="$TMP/out"

expect_rc() {
local expected="$1" rc
shift
if "$@" > "$OUT" 2>&1; then rc=0; else rc=$?; fi
if [[ "$rc" -ne "$expected" ]]; then
echo "FAIL: expected exit $expected, got $rc: $*" >&2
cat "$OUT" >&2
exit 1
fi
}

# An invalid request must never take a lock.
expect_rc 2 bash "$HELPER" acquire "$LOCK"
expect_rc 2 bash "$HELPER" acquire "$LOCK" bad not-a-number
[[ ! -e "$LOCK" ]]

# Owner exclusivity, wrong-owner release and proper cleanup.
expect_rc 0 bash "$HELPER" acquire "$LOCK" deploy-A
[[ "$(cut -d' ' -f1 "$LOCK/owner")" == "deploy-A" ]]
expect_rc 3 bash "$HELPER" acquire "$LOCK" deploy-B
expect_rc 0 bash "$HELPER" release "$LOCK" deploy-B
[[ -d "$LOCK" && "$(cut -d' ' -f1 "$LOCK/owner")" == "deploy-A" ]]
expect_rc 0 bash "$HELPER" release "$LOCK" deploy-A
[[ ! -e "$LOCK" ]]

# An old lock must not be stolen, even when its age exceeds the threshold.
expect_rc 0 bash "$HELPER" acquire "$LOCK" deploy-old
touch -t 202001010000.00 "$LOCK/owner"
expect_rc 3 bash "$HELPER" acquire "$LOCK" deploy-new 1
grep -q 'STALE' "$OUT"
[[ "$(cut -d' ' -f1 "$LOCK/owner")" == "deploy-old" ]]
expect_rc 0 bash "$HELPER" release "$LOCK" deploy-old

# Concurrent invocations may admit exactly one owner.
SUCCESS="$TMP/successes"
: > "$SUCCESS"
for i in 1 2 3 4 5 6 7 8 9 10 11 12; do
(
if bash "$HELPER" acquire "$LOCK" "worker-$i" > /dev/null 2>&1; then
printf '%s\n' "$i" >> "$SUCCESS"
fi
) &
done
wait
[[ "$(wc -l < "$SUCCESS" | tr -d ' ')" == "1" ]] || {
echo "FAIL: simultaneous lock holders detected" >&2
cat "$SUCCESS" >&2
exit 1
}
winner="$(cut -d' ' -f1 "$LOCK/owner")"
[[ "$winner" == worker-* ]]
expect_rc 0 bash "$HELPER" release "$LOCK" "$winner"
[[ ! -e "$LOCK" ]]

echo "PASS: shared deploy lock ownership, stale refusal and 12-way concurrency"
46 changes: 46 additions & 0 deletions scripts/check-postmerge-embed-release.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
# Release gate for post-merge PR #41. No GitHub Actions, merge or deploy.
set -Eeuo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
EXPECTED_BRANCH="fix/toolify-postmerge-embed-safety-20261107"
ACTUAL_BRANCH="$(git -C "$ROOT" branch --show-current)"
if [[ "$ACTUAL_BRANCH" != "$EXPECTED_BRANCH" ]]; then
echo "[postmerge] ERROR: expected $EXPECTED_BRANCH, found ${ACTUAL_BRANCH:-detached}" >&2
exit 2
fi
# Fetch the remote-tracking ref we actually verify, instead of trusting an old local ref.
git -C "$ROOT" fetch --no-tags origin main:refs/remotes/origin/main
if ! git -C "$ROOT" merge-base --is-ancestor refs/remotes/origin/main HEAD; then
echo "[postmerge] ERROR: latest origin/main is not merged into this PR branch." >&2
exit 3
fi
# Check EVERY GitHub workflow for manual-only triggers using one fail-closed policy.
# Do not execute GitHub Actions; inspect workflow YAML locally.
node "$ROOT/scripts/verify-manual-deploy.mjs" "$ROOT/.github/workflows"
node --test "$ROOT/scripts/verify-manual-deploy.test.mjs"
# Exercise the shared VPS deploy lock safely against temporary local paths.
bash "$ROOT/ops/tests/deploy-lock.test.sh"
# The site config falls back to example.pages.dev when the public origin is unset.
# Pin this production release gate to the canonical origin to prevent wrong
# canonical, share and iframe URLs in an otherwise successful Next build.
export NEXT_PUBLIC_SITE_URL="https://toolify365.com"
cd "$ROOT/site"
echo "[postmerge] production origin=$NEXT_PUBLIC_SITE_URL"
echo "[postmerge] node=$(node --version) npm=$(npm --version)"
NODE_MAJOR="$(node -p 'process.versions.node.split(".")[0]')"
if (( NODE_MAJOR < 22 )); then
echo "[postmerge] ERROR: Node >=22 required for dependency-free TypeScript regression." >&2
exit 2
fi
node --experimental-strip-types --test scripts/embed-html-node.test.mjs scripts/share-errors-node.test.mjs
npm ci
npm run test:smoke-html
npm run test:integration
npm run typecheck
npm test
npm run validate
npm run audit:seo
npm run audit:i18n
npm run audit:index-policy -- --json
npm run build
echo "[postmerge] PASS: static tests and build; browser, consent and deploy checks remain manual."
82 changes: 82 additions & 0 deletions scripts/verify-manual-deploy.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { readFileSync, readdirSync, statSync } from "node:fs";
import { join } from "node:path";
import { pathToFileURL } from "node:url";

/**
* Fail closed on any non-manual GitHub Actions trigger.
* Intentionally accepts only the repository's simple YAML representation:
*
* on:
* workflow_dispatch: {}
*
* We avoid regex-only push detection: on: [push] and on: push also deploy.
* Any unrecognized event syntax must be reviewed before a release.
*/
export function verifyManualDeployWorkflow(source) {
const lines = source.replace(/\r\n/g, "\n").split("\n");
let blocks = 0;
let manual = 0;
let inEvents = false;

for (let index = 0; index < lines.length; index += 1) {
const line = lines[index];
if (/^\s*(?:#.*)?$/.test(line)) continue;

const root = line.match(/^(on|'on'|"on")\s*:(.*)$/);
if (root) {
blocks += 1;
if (root[1] !== "on" || root[2].trim()) {
throw new Error("Only a plain multiline on: event block is allowed");
}
inEvents = true;
continue;
}
if (/^\S/.test(line)) {
inEvents = false;
continue;
}
if (inEvents) {
if (/^ workflow_dispatch: \{\}(?:\s*#.*)?$/.test(line)) {
manual += 1;
} else {
throw new Error(`Unexpected workflow trigger at line ${index + 1}: ${line.trim()}`);
}
}
}
if (blocks !== 1 || manual !== 1) {
throw new Error("Workflow must have exactly one manual-only on: block");
}
return true;
}

/** Validate every workflow in a directory; a single safe deploy.yml is not sufficient. */
export function verifyWorkflowPath(path) {
const files = statSync(path).isDirectory()
? readdirSync(path).filter((entry) => /\.ya?ml$/i.test(entry)).sort().map((entry) => join(path, entry))
: [path];
if (files.length === 0) throw new Error("No YAML workflows were found");
for (const file of files) {
try {
verifyManualDeployWorkflow(readFileSync(file, "utf8"));
} catch (error) {
throw new Error(`${file}: ${error.message}`);
}
}
return files.length;
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const file = process.argv[2];
if (!file) {
process.stderr.write("Usage: node verify-manual-deploy.mjs <workflow.yml>\n");
process.exitCode = 2;
} else {
try {
const count = verifyWorkflowPath(file);
process.stdout.write(`[postmerge] PASS: ${count} manual-only GitHub workflow trigger(s)\n`);
} catch (error) {
process.stderr.write(`[postmerge] ERROR: ${error.message}\n`);
process.exitCode = 4;
}
}
}
Loading