Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
66 changes: 66 additions & 0 deletions .github/workflows/deploy_promotion.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: Deploy promotion banner

on:
workflow_dispatch:

permissions:
contents: read
id-token: write

concurrency:
group: deploy-promotion-banner
cancel-in-progress: false

jobs:
deploy:
runs-on: ubuntu-latest
environment: prod
steps:
- uses: actions/checkout@v4
- name: Validate campaign
run: |
node --check theme/discount.js
node --test scripts/test_discount.js
- name: Configure AWS credentials using OIDC
uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: us-east-1
- name: Update promotion on every deployed language
shell: bash
env:
BUCKET: hacktricks-cloud
DISTRIBUTION_ID: ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }}
run: |
set -euo pipefail
# Discover only existing language roots; leave book content untouched.
aws s3api list-objects-v2 --bucket "$BUCKET" --delimiter / \
--query 'CommonPrefixes[].Prefix' --output json > /tmp/prefixes.json
python3 - <<'PYCODE'
import json, re
prefixes = [p for p in json.load(open('/tmp/prefixes.json')) if re.fullmatch(r'[a-z]{2}/', p)]
if 'en/' not in prefixes:
raise SystemExit('Expected the deployed English book')
with open('/tmp/languages.txt', 'w') as output:
output.write('\n'.join(prefixes) + '\n')
PYCODE
while IFS= read -r prefix; do
aws s3 cp src/images/hacktricks-black-friday-2026-v1.webp \
"s3://${BUCKET}/${prefix}images/hacktricks-black-friday-2026-v1.webp" \
--content-type image/webp --cache-control 'public,max-age=31536000,immutable' --only-show-errors
# mdBook versions have emitted both paths. Cover both existing page layouts.
for script in theme/discount.js discount.js; do
aws s3 cp theme/discount.js "s3://${BUCKET}/${prefix}${script}" \
--content-type application/javascript --cache-control 'public,max-age=300' --only-show-errors
done
for stylesheet in theme/discount.css discount.css; do
aws s3 cp theme/discount.css "s3://${BUCKET}/${prefix}${stylesheet}" \
--content-type text/css --cache-control 'public,max-age=300' --only-show-errors
done
done < /tmp/languages.txt
invalidation_id=$(aws cloudfront create-invalidation \
--distribution-id "$DISTRIBUTION_ID" \
--paths '/*/theme/discount.js' '/*/discount.js' '/*/theme/discount.css' '/*/discount.css' \
--query Invalidation.Id --output text)
aws cloudfront wait invalidation-completed \
--distribution-id "$DISTRIBUTION_ID" --id "$invalidation_id"
6 changes: 6 additions & 0 deletions .github/workflows/translate_all.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ jobs:
mkdir -p /tmp/immutable-images
cp \
src/images/hacktricks-summer-discount-2026-v1.webp \
src/images/hacktricks-black-friday-2026-v1.webp \
src/images/arte-badge-v1.webp \
src/images/grte-badge-v1.webp \
src/images/azrte-badge-v1.webp \
Expand All @@ -120,6 +121,7 @@ jobs:
src/images/arte-sponsor-v1.webp \
/tmp/immutable-images/
cp theme/discount.js /tmp/discount.js
cp theme/discount.css /tmp/discount.css

- name: Run get_and_save_refs.py
run: |
Expand Down Expand Up @@ -213,6 +215,10 @@ jobs:
MDBOOK_BOOK__LANGUAGE=$BRANCH mdbook build || (echo "Error logs" && cat hacktricks-preprocessor-error.log && echo "" && echo "" && echo "Debug logs" && (cat hacktricks-preprocessor.log | tail -n 20) && exit 1)
cp /tmp/immutable-images/* ./book/images/
cp /tmp/discount.js ./book/discount.js
mkdir -p ./book/theme
cp /tmp/discount.js ./book/theme/discount.js
cp /tmp/discount.css ./book/discount.css
cp /tmp/discount.css ./book/theme/discount.css
cp /tmp/ht_searcher.js ./book/theme/ht_searcher.js

- name: Push search index to hacktricks-searchindex repo
Expand Down
110 changes: 110 additions & 0 deletions scripts/test_discount.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
const assert = require('node:assert/strict')
const { readFileSync } = require('node:fs')
const { test } = require('node:test')
const vm = require('node:vm')

const source = readFileSync(require('node:path').join(__dirname, '../theme/discount.js'), 'utf8')
const KEY = 'htDiscountDismissedUntil:black-friday-2026'
const TEN_DAYS = 10 * 24 * 60 * 60 * 1000

function load(date, { storage = new Map(), blocked = false, search = '', loading = false } = {}) {
const now = Date.parse(date)
const nodes = []
const listeners = new Map()
function element() {
const node = {
style: { setProperty() {} },
children: [], attributes: {}, handlers: {}, isConnected: false,
classList: { contains: () => false, add() {}, remove() {} },
setAttribute(name, value) { this.attributes[name] = value },
append(...children) { children.forEach(child => this.appendChild(child)) },
appendChild(child) { this.children.push(child); child.isConnected = true },
addEventListener(name, fn) { this.handlers[name] = fn },
remove() { this.isConnected = false },
}
nodes.push(node)
return node
}
const document = {
readyState: loading ? 'loading' : 'complete',
baseURI: 'https://cloud.hacktricks.wiki/es/nested/page.html',
documentElement: element(), body: element(), createElement: element,
querySelector: () => nodes.find(n => n.className === 'ht-discount-overlay' && n.isConnected),
addEventListener: (name, fn) => listeners.set(name, fn),
removeEventListener: name => listeners.delete(name),
}
const window = {
path_to_root: '../', location: { search, href: document.baseURI },
matchMedia: () => ({ matches: true }), setTimeout: fn => fn(),
localStorage: {
getItem(key) { if (blocked) throw Error('blocked'); return storage.get(key) ?? null },
setItem(key, value) { if (blocked) throw Error('blocked'); storage.set(key, value) },
},
}
class Clock extends Date { static now() { return now } }
vm.runInNewContext(source, { window, document, Date: Clock, URL, URLSearchParams })
return { nodes, storage, listeners, overlay: () => document.querySelector(), find: name => nodes.find(n => n.className === name) }
}

test('campaign appears only throughout November 2026, including UTC boundaries', () => {
for (const date of ['2026-11-01T00:00:00Z', '2026-11-30T23:59:59.999Z']) {
assert.ok(load(date).overlay(), date)
}
for (const date of ['2026-10-31T23:59:59.999Z', '2026-12-01T00:00:00Z', '2026-07-15T12:00:00Z', '2027-11-08T12:00:00Z']) {
const page = load(date)
assert.equal(page.overlay(), undefined, date)
assert.equal(page.find('ht-discount-image'), undefined, 'inactive campaigns must not download the image')
}
})

test('translated nested pages use their language root and the training courses link', () => {
const page = load('2026-11-08T12:00:00Z')
assert.equal(page.find('ht-discount-image').src, 'https://cloud.hacktricks.wiki/es/images/hacktricks-black-friday-2026-v1.webp')
assert.match(page.find('ht-discount-image').alt, /20% off courses, 15% off labs and exams/)
assert.equal(page.find('ht-discount-primary').href, 'https://hacktricks-training.com/courses')
})

test('hide persists across navigation and expires exactly after ten days', () => {
const page = load('2026-11-08T12:00:00Z')
page.find('ht-discount-delay').handlers.click()
assert.equal(page.overlay(), undefined)
assert.equal(Number(page.storage.get(KEY)), Date.parse('2026-11-08T12:00:00Z') + TEN_DAYS)
assert.equal(load('2026-11-18T11:59:59.999Z', page).overlay(), undefined)
assert.ok(load('2026-11-18T12:00:00Z', page).overlay())
})

test('summer dismissal does not hide Black Friday and preview overrides its dismissal', () => {
const storage = new Map([['htDiscountDismissedUntil:summer-2026', String(Date.parse('2026-12-01T00:00:00Z'))]])
assert.ok(load('2026-11-08T12:00:00Z', { storage }).overlay())
storage.set(KEY, String(Date.parse('2026-11-20T12:00:00Z')))
assert.equal(load('2026-11-08T12:00:00Z', { storage }).overlay(), undefined)
assert.ok(load('2026-11-08T12:00:00Z', { storage, search: '?discount=show' }).overlay())
assert.equal(load('2026-12-01T00:00:00Z', { storage, search: '?discount=show' }).overlay(), undefined)
})

test('close, Escape and outside click close only the current page', () => {
for (const method of ['close', 'escape', 'outside']) {
const page = load('2026-11-08T12:00:00Z')
if (method === 'close') page.find('ht-discount-close').handlers.click()
if (method === 'escape') page.listeners.get('keydown')({ key: 'Escape' })
if (method === 'outside') page.overlay().handlers.click({ target: page.overlay() })
assert.equal(page.overlay(), undefined)
assert.equal(page.listeners.has('keydown'), false)
assert.equal(page.storage.size, 0)
assert.ok(load('2026-11-08T12:00:01Z', page).overlay())
}
})

test('blocked storage keeps the page and close controls working', () => {
const page = load('2026-11-08T12:00:00Z', { blocked: true })
assert.ok(page.overlay())
assert.doesNotThrow(() => page.find('ht-discount-delay').handlers.click())
assert.equal(page.overlay(), undefined)
})

test('initial page load waits for the DOM', () => {
const page = load('2026-11-08T12:00:00Z', { loading: true })
assert.equal(page.overlay(), undefined)
page.listeners.get('DOMContentLoaded')()
assert.ok(page.overlay())
})
1 change: 1 addition & 0 deletions scripts/upload_immutable_images.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ cache_control="public,max-age=31536000,immutable"

for image in \
hacktricks-summer-discount-2026-v1.webp \
hacktricks-black-friday-2026-v1.webp \
arte-badge-v1.webp \
grte-badge-v1.webp \
azrte-badge-v1.webp \
Expand Down
Binary file added src/images/hacktricks-black-friday-2026-v1.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion theme/discount.css
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
width: min(92vw, 520px);
max-height: min(92vh, 760px);
isolation: isolate;
overflow: hidden;
overflow-x: hidden;
overflow-y: auto;
border-radius: 16px;
padding: 12px;
background: transparent;
Expand Down
19 changes: 10 additions & 9 deletions theme/discount.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
;(function discount() {
var CAMPAIGN_ID = "summer-" + new Date().getFullYear()
var CAMPAIGN_ID = "black-friday-2026"
var DISMISS_KEY = "htDiscountDismissedUntil:" + CAMPAIGN_ID
var DISMISS_MS = 10 * 24 * 60 * 60 * 1000
var IMAGE_PATH = "images/hacktricks-summer-discount-2026-v1.webp"
var TARGET_URL = "https://hacktricks-training.com"
var TITLE = "HackTricks Training Summer Discount"
var IMAGE_PATH = "images/hacktricks-black-friday-2026-v1.webp"
var TARGET_URL = "https://hacktricks-training.com/courses"
var TITLE = "HackTricks Training Black Friday 2026 — 20% off courses, 15% off labs and exams, plus 10% off bundles"

function storageGet(key) {
try {
Expand Down Expand Up @@ -45,8 +45,9 @@
}

function isCampaignActive() {
// getMonth() is zero-based; 6 covers July 1 through July 31 in the visitor's local time.
return new Date().getMonth() === 6
// Match the training campaign: all of November 2026, using UTC boundaries.
var now = Date.now()
return now >= Date.UTC(2026, 10, 1) && now < Date.UTC(2026, 11, 1)
}

function isDismissed() {
Expand All @@ -68,7 +69,7 @@
}, 220)
}

function dismissFor15Days(overlay, onKeydown) {
function dismissFor10Days(overlay, onKeydown) {
storageSet(DISMISS_KEY, String(Date.now() + DISMISS_MS))
close(overlay, onKeydown)
}
Expand Down Expand Up @@ -136,7 +137,7 @@

var kicker = document.createElement("span")
kicker.className = "ht-discount-kicker"
kicker.textContent = "Summer offer"
kicker.textContent = "Black Friday offer"

var closeButton = document.createElement("button")
closeButton.className = "ht-discount-close"
Expand Down Expand Up @@ -191,7 +192,7 @@
})

delayButton.addEventListener("click", function() {
dismissFor15Days(overlay, onKeydown)
dismissFor10Days(overlay, onKeydown)
})

overlay.addEventListener("click", function(event) {
Expand Down
Loading