Skip to content

Commit 7a6d3bd

Browse files
authored
Merge pull request #5 from Cloud2BR-MSFTLearningHub/chore/update-org-learning-hub-name
chore: align badges and workflows with org template
2 parents 89372d5 + 1e3b598 commit 7a6d3bd

20 files changed

Lines changed: 631 additions & 111 deletions

.github/.markdownlint.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"default": true,
3+
"MD013": false,
4+
"MD033": false,
5+
"MD041": false
6+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
from __future__ import annotations
2+
3+
import os
4+
import re
5+
import subprocess
6+
from datetime import datetime, timezone
7+
from pathlib import Path
8+
9+
DATE_PATTERN = re.compile(r"^Last updated: \d{4}-\d{2}-\d{2}$", re.MULTILINE)
10+
CURRENT_DATE = datetime.now(timezone.utc).strftime("%Y-%m-%d")
11+
REPO_ROOT = Path(__file__).resolve().parents[2]
12+
13+
14+
def run_git(*args: str) -> str:
15+
result = subprocess.run(
16+
["git", *args],
17+
cwd=REPO_ROOT,
18+
check=True,
19+
capture_output=True,
20+
text=True,
21+
)
22+
return result.stdout.strip()
23+
24+
25+
def changed_markdown_files() -> list[Path]:
26+
base_ref = os.environ.get("PR_BASE_REF", "main")
27+
diff_output = run_git("diff", "--name-only", f"origin/{base_ref}...HEAD", "--", "*.md")
28+
files = []
29+
for relative_path in diff_output.splitlines():
30+
path = REPO_ROOT / relative_path
31+
if path.is_file():
32+
files.append(path)
33+
return files
34+
35+
36+
def update_file(path: Path) -> bool:
37+
content = path.read_text(encoding="utf-8")
38+
updated_content, count = DATE_PATTERN.subn(f"Last updated: {CURRENT_DATE}", content, count=1)
39+
if count == 0 or updated_content == content:
40+
return False
41+
path.write_text(updated_content, encoding="utf-8")
42+
return True
43+
44+
45+
def main() -> int:
46+
files = changed_markdown_files()
47+
if not files:
48+
print("No changed Markdown files detected.")
49+
return 0
50+
51+
updated_any = False
52+
for path in files:
53+
if update_file(path):
54+
print(f"Updated {path.relative_to(REPO_ROOT)}")
55+
updated_any = True
56+
57+
if not updated_any:
58+
print("No Last updated lines needed changes.")
59+
return 0
60+
61+
62+
if __name__ == "__main__":
63+
raise SystemExit(main())
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
const https = require('https');
4+
5+
const repoRoot = path.resolve(__dirname, '..', '..');
6+
const metricsPath = path.join(repoRoot, 'metrics.json');
7+
const refreshDate = new Date().toISOString().slice(0, 10);
8+
const repo = process.env.REPO;
9+
const token = process.env.TRAFFIC_TOKEN;
10+
const markerPattern = /<!-- START BADGE -->[\s\S]*?<!-- END BADGE -->/g;
11+
12+
function readMetrics() {
13+
if (!fs.existsSync(metricsPath)) {
14+
return { totalViews: 0, refreshDate };
15+
}
16+
17+
try {
18+
return JSON.parse(fs.readFileSync(metricsPath, 'utf8'));
19+
} catch {
20+
return { totalViews: 0, refreshDate };
21+
}
22+
}
23+
24+
function writeMetrics(metrics) {
25+
fs.writeFileSync(metricsPath, `${JSON.stringify(metrics, null, 2)}\n`, 'utf8');
26+
}
27+
28+
function fetchTrafficViews() {
29+
if (!repo || !token) {
30+
return Promise.resolve(null);
31+
}
32+
33+
return new Promise((resolve) => {
34+
const request = https.request(
35+
{
36+
hostname: 'api.github.com',
37+
path: `/repos/${repo}/traffic/views`,
38+
method: 'GET',
39+
headers: {
40+
'User-Agent': 'Cloud2BR-visitor-counter',
41+
Accept: 'application/vnd.github+json',
42+
Authorization: `Bearer ${token}`,
43+
},
44+
},
45+
(response) => {
46+
let data = '';
47+
response.on('data', (chunk) => {
48+
data += chunk;
49+
});
50+
response.on('end', () => {
51+
if (response.statusCode !== 200) {
52+
console.warn(`GitHub traffic API returned ${response.statusCode}.`);
53+
resolve(null);
54+
return;
55+
}
56+
57+
try {
58+
const parsed = JSON.parse(data);
59+
resolve(typeof parsed.count === 'number' ? parsed.count : null);
60+
} catch {
61+
resolve(null);
62+
}
63+
});
64+
}
65+
);
66+
67+
request.on('error', () => resolve(null));
68+
request.end();
69+
});
70+
}
71+
72+
function findMarkdownFiles(startDir) {
73+
const results = [];
74+
for (const entry of fs.readdirSync(startDir, { withFileTypes: true })) {
75+
if (entry.name === '.git' || entry.name === 'node_modules') {
76+
continue;
77+
}
78+
79+
const fullPath = path.join(startDir, entry.name);
80+
if (entry.isDirectory()) {
81+
results.push(...findMarkdownFiles(fullPath));
82+
continue;
83+
}
84+
85+
if (entry.isFile() && entry.name.endsWith('.md')) {
86+
results.push(fullPath);
87+
}
88+
}
89+
return results;
90+
}
91+
92+
function buildBadgeBlock(totalViews) {
93+
return [
94+
'<!-- START BADGE -->',
95+
'<div align="center">',
96+
` <img src="https://img.shields.io/badge/Total%20views-${totalViews}-limegreen" alt="Total views">`,
97+
` <p>Refresh Date: ${refreshDate}</p>`,
98+
'</div>',
99+
'<!-- END BADGE -->',
100+
].join('\n');
101+
}
102+
103+
function updateMarkdownBadges(totalViews) {
104+
const replacement = buildBadgeBlock(totalViews);
105+
for (const filePath of findMarkdownFiles(repoRoot)) {
106+
const content = fs.readFileSync(filePath, 'utf8');
107+
if (!markerPattern.test(content)) {
108+
markerPattern.lastIndex = 0;
109+
continue;
110+
}
111+
112+
markerPattern.lastIndex = 0;
113+
const updated = content.replace(markerPattern, replacement);
114+
fs.writeFileSync(filePath, updated, 'utf8');
115+
}
116+
}
117+
118+
async function main() {
119+
const currentMetrics = readMetrics();
120+
const fetchedViews = await fetchTrafficViews();
121+
const totalViews = fetchedViews ?? currentMetrics.totalViews ?? 0;
122+
const nextMetrics = { totalViews, refreshDate };
123+
124+
writeMetrics(nextMetrics);
125+
updateMarkdownBadges(totalViews);
126+
console.log(`Visitor badge refreshed with ${totalViews} total views.`);
127+
}
128+
129+
main().catch((error) => {
130+
console.error(error);
131+
process.exit(1);
132+
});
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
from __future__ import annotations
2+
3+
import re
4+
import subprocess
5+
from pathlib import Path
6+
7+
REPO_ROOT = Path(__file__).resolve().parents[2]
8+
GITHUB_BADGE = "[![GitHub](https://img.shields.io/badge/--181717?logo=github&logoColor=ffffff)](https://github.com/)"
9+
ORG_LINK = "[Cloud2BR OSS - Learning Hub](https://github.com/Cloud2BR-MSFTLearningHub)"
10+
DATE_RE = re.compile(r"Last updated: \d{4}-\d{2}-\d{2}")
11+
SEPARATOR_RE = re.compile(r"-{10,}")
12+
13+
14+
def tracked_markdown_files() -> list[Path]:
15+
result = subprocess.run(
16+
["git", "ls-files", "*.md"],
17+
cwd=REPO_ROOT,
18+
check=True,
19+
capture_output=True,
20+
text=True,
21+
)
22+
return [REPO_ROOT / line for line in result.stdout.splitlines() if line]
23+
24+
25+
def validate_file(path: Path) -> list[str]:
26+
lines = path.read_text(encoding="utf-8").splitlines()
27+
errors: list[str] = []
28+
29+
if not lines or not lines[0].startswith("# "):
30+
return ["first line must be a markdown title starting with '# '"]
31+
32+
if len(lines) < 8:
33+
return ["file is too short to contain the required header block"]
34+
35+
if lines[1] != "":
36+
errors.append("line 2 must be blank")
37+
38+
location_index = 2
39+
while location_index < len(lines) and lines[location_index].strip():
40+
location_index += 1
41+
42+
if location_index == 2:
43+
errors.append("location line is missing")
44+
return errors
45+
46+
if location_index >= len(lines) or lines[location_index] != "":
47+
errors.append("blank line required after location block")
48+
return errors
49+
50+
header_start = location_index + 1
51+
expected = [GITHUB_BADGE, ORG_LINK, "", None, "", None]
52+
for offset, expected_line in enumerate(expected):
53+
line_index = header_start + offset
54+
if line_index >= len(lines):
55+
errors.append("header block is incomplete")
56+
return errors
57+
actual = lines[line_index]
58+
if offset == 3:
59+
if not DATE_RE.fullmatch(actual):
60+
errors.append("Last updated line must use YYYY-MM-DD format")
61+
elif offset == 5:
62+
if not SEPARATOR_RE.fullmatch(actual):
63+
errors.append("separator line must contain at least 10 hyphens")
64+
elif actual != expected_line:
65+
errors.append(f"expected '{expected_line}' at line {line_index + 1}")
66+
67+
return errors
68+
69+
70+
def main() -> int:
71+
failures = []
72+
for path in tracked_markdown_files():
73+
errors = validate_file(path)
74+
if errors:
75+
failures.append((path.relative_to(REPO_ROOT), errors))
76+
77+
if failures:
78+
for path, errors in failures:
79+
print(f"{path}:")
80+
for error in errors:
81+
print(f" - {error}")
82+
return 1
83+
84+
print("All tracked Markdown files passed header validation.")
85+
return 0
86+
87+
88+
if __name__ == "__main__":
89+
raise SystemExit(main())

.github/workflows/update-md-date.yml

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,37 +5,61 @@ on:
55
branches:
66
- main
77

8+
concurrency:
9+
group: pr-branch-maintenance-${{ github.event.pull_request.head.ref || github.ref_name }}
10+
cancel-in-progress: false
11+
812
permissions:
913
contents: write
14+
pull-requests: write
1015

1116
jobs:
1217
update-date:
1318
runs-on: ubuntu-latest
1419

1520
steps:
16-
- name: Checkout repository
21+
- name: Checkout PR branch
1722
uses: actions/checkout@v4
1823
with:
1924
fetch-depth: 0
25+
ref: ${{ github.event.pull_request.head.ref }}
26+
27+
- name: Fetch PR base branch
28+
run: git fetch --no-tags origin ${{ github.event.pull_request.base.ref }}
2029

2130
- name: Set up Python
22-
uses: actions/setup-python@v4
31+
uses: actions/setup-python@v5
2332
with:
24-
python-version: '3.x'
25-
26-
- name: Install dependencies
27-
run: pip install python-dateutil
33+
python-version: '3.12'
2834

29-
- name: Configure Git
35+
- name: Configure Git
3036
run: |
3137
git config --global user.email "github-actions[bot]@users.noreply.github.com"
3238
git config --global user.name "github-actions[bot]"
3339
3440
- name: Update last modified date in Markdown files
35-
run: python .github/workflows/update_date.py
41+
env:
42+
PR_BASE_REF: ${{ github.event.pull_request.base.ref }}
43+
run: python .github/scripts/update_markdown_dates.py
3644

37-
- name: Commit changes
45+
- name: Pull, commit, and push if needed
46+
env:
47+
TOKEN: ${{ secrets.GITHUB_TOKEN }}
3848
run: |
49+
BRANCH="${{ github.event.pull_request.head.ref }}"
3950
git add -A
40-
git commit -m "Update last modified date in Markdown files" || echo "No changes to commit"
41-
git push origin HEAD:${{ github.event.pull_request.head.ref }}
51+
if git diff --staged --quiet; then
52+
echo "No changes to commit"
53+
exit 0
54+
fi
55+
git commit -m "Update last modified date in Markdown files"
56+
git remote set-url origin https://x-access-token:${TOKEN}@github.com/${{ github.repository }}
57+
for attempt in 1 2 3; do
58+
git fetch origin "$BRANCH"
59+
git rebase "origin/$BRANCH"
60+
if git push origin HEAD:"$BRANCH"; then
61+
exit 0
62+
fi
63+
done
64+
echo "Failed to push branch updates after 3 attempts."
65+
exit 1

0 commit comments

Comments
 (0)