Skip to content
Open
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
50 changes: 50 additions & 0 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Build and deploy Pages

on:
push:
branches: [main]
pull_request:
workflow_dispatch:

permissions:
contents: read

concurrency:
group: pages-${{ github.ref }}
cancel-in-progress: false

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: '3.12'
- name: Resolve and validate index URLs
run: python scripts/build_index.py --output "${{ runner.temp }}/index.html"
- name: Stage static site
run: |
mkdir _site
rsync -a --exclude='/.git/' --exclude='/.github/' --exclude='/scripts/' --exclude='/_site/' ./ _site/
cp "${{ runner.temp }}/index.html" _site/index.html
touch _site/.nojekyll
- uses: actions/upload-pages-artifact@v4
if: github.event_name != 'pull_request'
with:
path: _site

deploy:
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
needs: build
runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- uses: actions/configure-pages@v5
- uses: actions/deploy-pages@v4
id: deployment
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,28 @@ Normative and historical IFC releases, including downloadable packages and indiv
* **Addendum** - improvements to existing features. The schema may change while maintaining upward compatibility.
* **Corrigendum** - documentation improvements. The schema does not change, although deprecations may be introduced.

## Website deployment

The root `index.html` is the source template. File URLs may use globs in
their filenames, for example `IFC/DEV/IFC4_4/HTML/*.exp`. Keep directory paths
explicit. Each file URL on `standards.buildingsmart.org` (and each relative
file URL) must match exactly one file in the checkout. Zero or multiple matches
fail the build; directory URLs are assumed valid and external URLs are unchanged.
Only URLs in the root index are processed, not links inside generated publications.

To generate and validate a separate index locally:

```sh
python scripts/build_index.py --output /tmp/standards-index.html
```

The Pages workflow validates pull requests and deploys pushes to `main`, staging
the full static repository with the generated index and preserving `CNAME`.
In repository **Settings > Pages > Build and deployment**, set **Source** to
**GitHub Actions** instead of deploying from a branch. Build automation that pushes
publication updates must use credentials that trigger workflows (for example a
GitHub App token or PAT); pushes made with `GITHUB_TOKEN` do not trigger push workflows.

## License

Copyright © buildingSMART International Ltd. All Rights reserved until formal release. Final IFC is licensed under the **Creative Commons Attribution-NoDerivatives 4.0 International License (CC BY-ND 4.0)**.
Expand Down
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ <h2 class="section-heading" id="standards-heading">Standards</h2>
<nav class="standards" aria-labelledby="standards-heading">
<section class="panel" aria-labelledby="ifc-heading">
<h3 class="panel-heading" id="ifc-heading"><span>IFC</span><span class="heading-description">Industry Foundation Classes</span></h3>
<div class="release release-row release-muted"><h4>4.4.0.0 (draft title)</h4><span class="status status-development">In development</span><span class="formats"><a href="https://standards.buildingsmart.org/IFC/DEV/IFC4_4/HTML/">HTML</a><a href="https://standards.buildingsmart.org/IFC/DEV/IFC4_4_PREVIEW/HTML/">HTML (All PRs preview)</a> <a href="https://standards.buildingsmart.org/IFC/DEV/IFC4_4/HTML/IFC4X3_DEV_dd97e9c.exp">EXPRESS</a> <a href="https://standards.buildingsmart.org/IFC/DEV/IFC4_4/HTML/IFC4X3_DEV_dd97e9c.xsd">XSD</a> <a href="https://standards.buildingsmart.org/IFC/DEV/IFC4_4/HTML/annex-a-psd.zip">PSD ZIP</a></span></div>
<div class="release release-row release-muted"><h4>4.4.0.0 (draft title)</h4><span class="status status-development">In development</span><span class="formats"><a href="https://standards.buildingsmart.org/IFC/DEV/IFC4_4/HTML/">HTML</a><a href="https://standards.buildingsmart.org/IFC/DEV/IFC4_4_PREVIEW/HTML/">HTML (All PRs preview)</a> <a href="https://standards.buildingsmart.org/IFC/DEV/IFC4_4/HTML/*.exp">EXPRESS</a> <a href="https://standards.buildingsmart.org/IFC/DEV/IFC4_4/HTML/*.xsd">XSD</a> <a href="https://standards.buildingsmart.org/IFC/DEV/IFC4_4/HTML/annex-a-psd.zip">PSD ZIP</a></span></div>
<div class="release release-row"><h4>4.3 ADD2 (4.3.2.0)</h4><span class="status status-final">Final</span>
<span class="formats"><a href="https://standards.buildingsmart.org/IFC/DEV/IFC4_3/HTML">HTML</a> <a href="https://standards.buildingsmart.org/IFC/RELEASE/IFC4_3/HTML/IFC4X3_ADD2.exp">EXPRESS</a> <a href="https://standards.buildingsmart.org/IFC/RELEASE/IFC4_3/HTML/IFC4X3_ADD2.xsd">XSD</a> <a href="https://standards.buildingsmart.org/IFC/DEV/IFC4_3/HTML/annex-a-psd.zip">PSD ZIP</a></span><p class="note"> NOTE: The HTML and PSD publication differs from the version submitted to ISO, as it contains grammatical corrections and description clarifications (without altering the structure or schema of the official publication). The website also includes MVD information and search, feedback, and translation features. You can download the official ISO release here: <a href="https://standards.buildingsmart.org/IFC/RELEASE/IFC4_3/IFC4.3.2.0.zip">IFC4.3.2.0.zip</a></p></div>
<div class="release release-row release-muted"><h4>4.1.0.0</h4><span class="status status-withdrawn">Withdrawn</span><span class="formats"><a href="https://standards.buildingsmart.org/IFC/RELEASE/IFC4_1/FINAL/HTML">HTML</a> <a href="https://standards.buildingsmart.org/IFC/RELEASE/IFC4_1/FINAL/EXPRESS/IFC4x1.exp">EXPRESS</a> <a href="https://standards.buildingsmart.org/IFC/RELEASE/IFC4_1/FINAL/XML/IFC4x1.xsd">XSD</a> <a href="https://standards.buildingsmart.org/IFC/RELEASE/IFC4_1/FINAL/IFC4_1-Final-HTML.zip">Full ZIP</a> <a href="https://standards.buildingsmart.org/IFC/RELEASE/IFC4_1/FINAL/XML/IFC4x1.xsd">XSD</a> <a href="https://standards.buildingsmart.org/IFC/RELEASE/IFC4_1/FINAL/ifc4x1-final_shortform.zip">Shortform ZIP</a></span></div>
Expand Down
69 changes: 69 additions & 0 deletions scripts/build_index.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""Resolve index.html file URLs against the checkout; directories are assumed valid."""

import argparse
import html
from pathlib import Path
import re
from urllib.parse import quote, unquote, urlsplit, urlunsplit


URL_ATTRIBUTE = re.compile(r'\b(?:href|src)\s*=\s*([\'"])(.*?)\1', re.IGNORECASE)
HOST = "standards.buildingsmart.org"


def render(source: str, root: Path) -> str:
errors = []
checked = 0

def replace(match):
nonlocal checked
url = urlsplit(html.unescape(match[2]))
if url.netloc and url.netloc.lower() != HOST:
return match[0]
if url.scheme and url.scheme not in ("http", "https"):
return match[0]
path = unquote(url.path).lstrip("/")
# Directory URLs (including extensionless HTML directories) stay intact.
if not path or not Path(path).suffix:
return match[0]
if ".." in Path(path).parts:
errors.append(f"Path escapes the site root: {path}")
return match[0]
matches = sorted(p for p in root.glob(path) if p.is_file())
checked += 1
if len(matches) != 1:
errors.append(f"{path}: expected exactly one file, found {len(matches)}"
+ "".join(f"\n {p.relative_to(root).as_posix()}" for p in matches))
return match[0]
resolved = quote(matches[0].relative_to(root).as_posix(), safe="/")
if url.path.startswith("/"):
resolved = "/" + resolved
new_url = urlunsplit((url.scheme, url.netloc, resolved, url.query, url.fragment))
start, end = match.start(2) - match.start(), match.end(2) - match.start()
return match[0][:start] + html.escape(new_url, quote=True) + match[0][end:]

result = URL_ATTRIBUTE.sub(replace, source)
if errors:
raise ValueError("URL validation failed:\n" + "\n".join(errors))
print(f"Validated {checked} file URLs (one match each).")
return result


def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[1])
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args()
root = args.root.resolve()
if args.output.resolve() == root / "index.html":
parser.error("output must differ from the source index.html")
try:
result = render((root / "index.html").read_text(encoding="utf-8"), root)
except ValueError as error:
parser.exit(1, f"{error}\n")
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(result, encoding="utf-8")


if __name__ == "__main__":
main()
Loading