Skip to content

Commit f5505aa

Browse files
committed
build: prepare inline processing with locked Hatch and Rocky model
1 parent 4e44d5c commit f5505aa

22 files changed

Lines changed: 3545 additions & 304 deletions

‎.dockerignore‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.git
2+
.env
3+
.env.*
4+
.venv
5+
.hatch
6+
dist
7+
__pycache__
8+
*.pyc
9+
site
10+
*.tif

‎.github/workflows/acceptance.yml‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: Processing model acceptance
2+
on:
3+
push:
4+
branches: ['feature/**']
5+
pull_request:
6+
workflow_call:
7+
permissions:
8+
contents: read
9+
jobs:
10+
processing:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
- uses: actions/setup-python@v5
15+
with:
16+
python-version: '3.12'
17+
- uses: astral-sh/setup-uv@v6
18+
- run: |
19+
uv pip install --system --require-hashes -r requirements/development.txt
20+
uv pip install --system --no-deps --no-build-isolation -e .
21+
- run: hatch run dev:check
22+
- run: hatch run test:test
23+
- run: |
24+
python scripts/prepare-inline.py --output /tmp/inline.cwl
25+
cwltool --validate /tmp/inline.cwl
26+
- run: |
27+
docker build -t inline-processing:test .
28+
docker run --rm inline-processing:test python -m app --help
29+
docker run --rm inline-processing:test python -c 'import importlib.util,os; assert os.getuid()==os.getgid()==2000; assert importlib.util.find_spec("pip") is None'

‎.github/workflows/build.yaml‎

Lines changed: 50 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -1,136 +1,54 @@
1-
name: build
1+
name: Candidate publication
22
on:
3-
push:
4-
branches:
5-
- main
6-
paths:
7-
- app.py
8-
- .github/**
9-
- docs/**
10-
- workflow.cwl.template
11-
- requirements.txt
12-
- Dockerfile
13-
3+
workflow_dispatch:
4+
inputs:
5+
publish_candidate:
6+
description: Publish candidate image after processing checks
7+
type: boolean
8+
default: false
9+
permissions:
10+
contents: read
1411
jobs:
15-
python-unit-test:
16-
12+
validate:
13+
uses: ./.github/workflows/acceptance.yml
14+
candidate:
15+
needs: validate
1716
runs-on: ubuntu-latest
17+
permissions:
18+
contents: read
19+
packages: write
1820
steps:
19-
- uses: actions/checkout@v2
20-
- uses: actions/setup-python@v2
21-
with:
22-
python-version: 3.x
23-
- run: pip install -r requirements.txt
24-
- run: pip install nose2
25-
- run: nose2
26-
27-
cwl-pack-validate:
28-
needs: python-unit-test
29-
runs-on: ubuntu-latest
30-
steps:
31-
- uses: actions/checkout@v2
32-
- uses: actions/setup-python@v2
33-
with:
34-
python-version: 3.x
35-
- run: pip install cwltool
36-
- run: content=`cat app.py` yq eval '.$graph[1].requirements.InitialWorkDirRequirement.listing[0].entry=strenv(content)' workflow.cwl.template > workflow.cwl
37-
- run: cwltool --validate workflow.cwl
38-
39-
version:
40-
needs: cwl-pack-validate
41-
42-
runs-on: ubuntu-latest
43-
44-
outputs:
45-
46-
app-version: ${{ steps.set-version.outputs.version }}
47-
48-
steps:
49-
- uses: actions/checkout@v2
50-
- run: echo "APP_VERSION=$(cat codemeta.json | jq -r .version )" >> $GITHUB_ENV
51-
- run: echo app version is $APP_VERSION
52-
- id: set-version
53-
run: echo "::set-output name=version::$APP_VERSION"
54-
55-
container-build:
56-
57-
needs: version
58-
59-
runs-on: ubuntu-latest
60-
61-
steps:
62-
- uses: actions/checkout@v2
63-
- run: echo version ${{needs.version.outputs.app-version}}
64-
- run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
65-
- name: build & push image
66-
run: |
67-
IMAGE_ID=ghcr.io/eoap/quickwin-inline-code/python-inline-code
68-
docker build . --file Dockerfile --tag $IMAGE_ID:${{needs.version.outputs.app-version}}
69-
docker push $IMAGE_ID:${{needs.version.outputs.app-version}}
70-
71-
create-release:
72-
73-
needs:
74-
- container-build
75-
- version
76-
77-
runs-on: ubuntu-latest
78-
79-
outputs:
80-
81-
upload_url: ${{ steps.set-upload-url.outputs.upload_url }}
82-
83-
steps:
84-
- name: release
85-
uses: actions/create-release@v1
86-
id: create_release
87-
with:
88-
draft: false
89-
prerelease: false
90-
release_name: ${{needs.version.outputs.app-version}}
91-
tag_name: ${{needs.version.outputs.app-version}}
92-
env:
93-
GITHUB_TOKEN: ${{ github.token }}
94-
- id: set-upload-url
95-
run: echo "::set-output name=upload_url::${{ steps.create_release.outputs.upload_url }}"
96-
97-
publish-artifacts:
98-
99-
needs:
100-
- create-release
101-
- version
102-
103-
runs-on: ubuntu-latest
104-
105-
steps:
106-
- uses: actions/checkout@v2
107-
- run: content=`cat app.py` yq eval '.$graph[1].requirements.InitialWorkDirRequirement.listing[0].entry=strenv(content)' workflow.cwl.template > workflow.cwl
108-
- run: |
109-
tag="ghcr.io/eoap/quickwin-inline-code/python-inline-code:${{needs.version.outputs.app-version}}"
110-
docker pull ${tag}
111-
shatag=$( docker inspect ${tag} | yq -r '.[0]["RepoDigests"][0]' )
112-
s="detect-water-body" t="${shatag}" yq -i eval '(.$graph[] | select (.id == env(s)) ).hints.DockerRequirement.dockerPull = env(t)' workflow.cwl
113-
114-
- run: |
115-
r=$( cat codemeta.json | jq -r ".codeRepository" ) yq -i eval '."s:codeRepository" = {"URL" : env(r)}' workflow.cwl
116-
v="${{needs.version.outputs.app-version}}" yq -i eval '."s:softwareVersion" = env(v)' workflow.cwl
117-
n=$(cat codemeta.json | jq -r '(.author[0].givenName + " " + .author[0].familyName)') \
118-
e=$(cat codemeta.json | jq -r '.author[0].email') \
119-
a=$(cat codemeta.json | jq -r '.author[0].affiliation["name"]') \
120-
yq eval -i '."s:author" += [{"class": "s:Person", "s.name": env(n), "s.email": env(e), "s.affiliation": env(a)}]' workflow.cwl
121-
- run: |
122-
mkdir downloads
123-
cp workflow.cwl downloads/app-water-body-inline-code.${{needs.version.outputs.app-version}}.cwl
124-
- uses: actions/upload-artifact@v4
125-
with:
126-
name: application-package
127-
path: downloads
128-
- name: upload linux artifact 1
129-
uses: actions/upload-release-asset@v1
130-
env:
131-
GITHUB_TOKEN: ${{ github.token }}
132-
with:
133-
upload_url: ${{needs.create-release.outputs.upload_url}}
134-
asset_path: downloads/app-water-body-inline-code.${{needs.version.outputs.app-version}}.cwl
135-
asset_name: app-water-body-inline-code.${{needs.version.outputs.app-version}}.cwl
136-
asset_content_type: text/yaml
21+
- uses: actions/checkout@v4
22+
- uses: actions/setup-python@v5
23+
with:
24+
python-version: '3.12'
25+
- uses: astral-sh/setup-uv@v6
26+
- run: uv pip install --system --require-hashes -r requirements/development.txt
27+
- name: Build the intended runtime
28+
run: |
29+
docker build -t ghcr.io/eoap/quickwin-inline-code/python-inline-code:2.0.0-rc.${{ github.run_number }} .
30+
docker run --rm ghcr.io/eoap/quickwin-inline-code/python-inline-code:2.0.0-rc.${{ github.run_number }} python -m app --help
31+
- uses: docker/login-action@v3
32+
if: inputs.publish_candidate
33+
with:
34+
registry: ghcr.io
35+
username: ${{ github.actor }}
36+
password: ${{ secrets.GITHUB_TOKEN }}
37+
- name: Publish candidate and prepare immutable CWL
38+
if: inputs.publish_candidate
39+
run: |
40+
image=ghcr.io/eoap/quickwin-inline-code/python-inline-code:2.0.0-rc.${{ github.run_number }}
41+
docker push "$image"
42+
published=$(docker inspect "$image" --format '{{index .RepoDigests 0}}')
43+
mkdir candidate
44+
python scripts/prepare-inline.py --image "$published" --version 2.0.0-rc.${{ github.run_number }} --output candidate/app-water-body-inline-code.cwl
45+
cwltool --validate candidate/app-water-body-inline-code.cwl
46+
cd candidate
47+
sha256sum app-water-body-inline-code.cwl > SHA256SUMS
48+
- uses: actions/upload-artifact@v4
49+
if: inputs.publish_candidate
50+
with:
51+
name: inline-code-candidate
52+
path: candidate
53+
# Stable promotion requires recorded real-data and immutable-artifact acceptance,
54+
# resolved license metadata, and explicit release authorization.

‎.gitignore‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,3 +165,12 @@ cython_debug/
165165
# and can be added to the global gitignore or merged into this file. For a more nuclear
166166
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
167167
#.idea/
168+
169+
.venv/
170+
.hatch/
171+
dist/
172+
__pycache__/
173+
*.pyc
174+
.pytest_cache/
175+
.mypy_cache/
176+
.ruff_cache/

‎Dockerfile‎

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,35 @@
1-
FROM docker.io/python:slim-bullseye
1+
FROM docker.io/rockylinux/rockylinux:10.1-minimal@sha256:cfa2812ec7151c2be393cfe46e50cf59426373d12f03f4ffbdd56becb8195a65 AS builder
2+
RUN microdnf -y update && \
3+
microdnf -y install curl tar git python3 python3-pip python3-setuptools gcc gcc-c++ make sqlite-devel zlib-devel glibc-devel libstdc++-devel && \
4+
microdnf clean all
5+
RUN test "$(uname -m)" = x86_64 && \
6+
curl -fsSL https://github.com/pypa/hatch/releases/download/hatch-v1.14.0/hatch-x86_64-unknown-linux-gnu.tar.gz -o /tmp/hatch.tar.gz && \
7+
echo "33af9b6f72cf11db6bc96070901cb9d32adbe316534bbf41d345cdd0d84b3459 /tmp/hatch.tar.gz" | sha256sum -c - && \
8+
tar -xzf /tmp/hatch.tar.gz -C /tmp && \
9+
install -m 0755 /tmp/hatch /usr/local/bin/hatch && \
10+
rm -rf /tmp/hatch*
11+
WORKDIR /src
12+
COPY . /src
13+
RUN rm -rf dist && hatch build -t wheel
214

3-
ADD requirements.txt /tmp/requirements.txt
4-
5-
RUN pip install --no-cache-dir -r /tmp/requirements.txt && \
6-
python -c "import rasterio" && \
7-
rm -rf /tmp/requirements.txt
8-
9-
ENTRYPOINT []
15+
FROM docker.io/rockylinux/rockylinux:10.1-minimal@sha256:cfa2812ec7151c2be393cfe46e50cf59426373d12f03f4ffbdd56becb8195a65 AS runtime
16+
RUN microdnf -y update && \
17+
microdnf -y install python3 python3-pip git jq libstdc++ sqlite-libs zlib libgomp expat && \
18+
microdnf clean all
19+
ENV HOME=/home/neo
20+
RUN /usr/sbin/groupadd -g 2000 neo && \
21+
/usr/sbin/useradd -u 2000 -g 2000 -m -d ${HOME} -s /sbin/nologin neo && \
22+
mkdir -p /app /work && chown -R 2000:2000 /app /work ${HOME}
23+
USER neo
24+
WORKDIR /app
25+
ENV VIRTUAL_ENV=/app/venv
26+
ENV PATH="${VIRTUAL_ENV}/bin:${PATH}:/usr/bin"
27+
RUN python3 -c 'import sys; assert sys.version_info[:2] == (3, 12)' && python3 -m venv /app/venv
28+
COPY --chown=2000:2000 requirements/runtime.txt /app/requirements.txt
29+
RUN pip install --no-cache-dir --require-hashes -r /app/requirements.txt
30+
COPY --from=builder --chown=2000:2000 /src/dist/*.whl /app/dist/
31+
RUN pip install --no-cache-dir --no-deps /app/dist/*.whl && \
32+
pip check && python -m pip uninstall --yes pip && detect-water-body-inline --help && python -m app --help && \
33+
rm -rf /app/requirements.txt /app/dist
34+
ENTRYPOINT []
35+
CMD ["detect-water-body-inline", "--help"]

‎README.md‎

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,15 @@ The CWL document generated is available in the [releases section](https://github
1414

1515
Update the Application Package template `workflow.cwl.template` with the Python `app.py` as the inline code to run:
1616

17+
```bash
18+
python scripts/prepare-inline.py --output workflow.cwl
1719
```
18-
content=`cat app.py` yq eval '.$graph[1].requirements.InitialWorkDirRequirement.listing[0].entry=strenv(content)' workflow.cwl.template > workflow.cwl
19-
```
20-
21-
Here's a breakdown of the command:
22-
23-
* `cat app.py`: This command outputs the contents of the `app.py` file.
2420

25-
* `yq eval '.$graph[1].requirements.InitialWorkDirRequirement.listing[0].entry=strenv(content)' workflow.cwl.template`: This command uses yq to evaluate a YAML expression on the `workflow.cwl.template` file. The expression modifies the entry at a specific path in the CWL `InitialWorkDirRequirement`, setting it to the content of `app.py`.
26-
27-
* `> workflow.cwl`: This part of the command redirects the modified output to a new file named `workflow.cwl`.
21+
The generated CWL v1.2 document embeds the complete standalone `app.py`, retains
22+
the published URI schema import, and includes its module-owned types inline.
23+
It accepts a URI record, named bounding-box coordinates and quoted CRS code
24+
`"4326"`; the command converts the code to `EPSG:4326` at the CLI boundary.
25+
Use `--image <local-tag-or-published-digest>` to test the intended image.
2826

2927
## Run the Application Package
3028

@@ -37,9 +35,17 @@ cwltool workflow.cwl params.yaml
3735
Where `params.yaml` contains:
3836

3937
```yaml
40-
item: "https://earth-search.aws.element84.com/v0/collections/sentinel-s2-l2a-cogs/items/S2B_10TFK_20210713_0_L2A"
41-
aoi: "-121.399,39.834,-120.74,40.472"
42-
epsg: "EPSG:4326"
38+
item:
39+
value: https://earth-search.aws.element84.com/v0/collections/sentinel-s2-l2a-cogs/items/S2B_10TFK_20210713_0_L2A
40+
aoi:
41+
west: -121.399
42+
south: 39.834
43+
east: -120.74
44+
north: 40.472
45+
epsg: '4326'
46+
bands:
47+
- green
48+
- nir
4349
```
4450
4551
## Run the released Application Package
@@ -97,4 +103,15 @@ The key components of this CWL document are:
97103

98104
The webpage of the documentation is https://eoap.github.io/quickwin-inline-code/.
99105

100-
[![License: CC BY-SA 4.0](https://img.shields.io/badge/License-CC_BY--SA_4.0-lightgrey.svg)](https://creativecommons.org/licenses/by-sa/4.0/)
106+
[![License: CC BY-SA 4.0](https://img.shields.io/badge/License-CC_BY--SA_4.0-lightgrey.svg)](https://creativecommons.org/licenses/by-sa/4.0/)
107+
## Package and runtime checks
108+
109+
Python 3.12 is required. Run `hatch run dev:check` and `hatch run test:test`
110+
for hash-locked quality and processing regressions. The wheel includes the same
111+
standalone `app.py` embedded in CWL, so inline execution remains supported.
112+
The two-stage Rocky 10.1 image installs the wheel and locked runtime, runs as
113+
`neo` (UID/GID 2000), verifies dependencies and removes venv pip.
114+
115+
The next major version is 2.0.0. Stable promotion still requires accepted immutable image/CWL
116+
artifacts and review of the existing license/author metadata: the checked-in
117+
license says CC BY-SA while CodeMeta currently says CC BY-NC-SA.

0 commit comments

Comments
 (0)