docs: preserve reviewed NetKingdom session artifacts
This commit is contained in:
parent
d9a74861ba
commit
cb60cd3bbe
45 changed files with 2502 additions and 0 deletions
Binary file not shown.
|
|
@ -0,0 +1,22 @@
|
|||
Source draft continuation, 2026-10-17 (Custodian preparation; no Apps assent):
|
||||
`tools/forgejo_configuration_prepare.py` now supplies offline frozen-source
|
||||
checks, exact proposed case-only OAuth/group-reference public projection,
|
||||
metadata-only upgrade/inverse command templates and original-before/post/current
|
||||
inverse guards. `tests/test_forgejo_configuration_prepare.py` exercises ten
|
||||
synthetic offline cases. The preparer does not authenticate an acceptance or
|
||||
observation, execute the helper/Helm, persist a transaction, or grant authority.
|
||||
`activate` always refuses. It does not change the existing delivery/hand-over
|
||||
receipt-only rollback or preexisting NOCHANGE paths.
|
||||
|
||||
Remaining T05 implementation is concrete: bind chart 12.5.0's actual public and
|
||||
private configuration projections; implement the owner committed-evidence
|
||||
journal's intent/post/inverse-attempt/outcome publication and recovery; bind a
|
||||
current attended owner operation session, with an independent inverse operation
|
||||
and disposition/revocation checks; enforce exclusion across every Forgejo
|
||||
release writer for check+mutation+readback. Existing Warden SQLite durability and
|
||||
process locking alone do not establish owner authentication or all-writer
|
||||
exclusion. Keep source dispatch disabled until these bindings exist. Preserve
|
||||
pending and publication-ambiguous outcomes for owner reconciliation. T02/T03
|
||||
still owe the four real login/group observations and T05 the real created-only
|
||||
objects/receipt backend. Existing needs-change history, human acceptance and
|
||||
first-case tasks remain open; this preparation does not close them.
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
# Apps T05 source preparation, r1
|
||||
|
||||
This packet adds real offline source mechanics on frozen Apps main
|
||||
`944b30ad63ab054cc24617e374e177d34cd572da`, without claiming an operational
|
||||
configuration backend. The isolated source is `/tmp/apps-execution-backend-r1/source`.
|
||||
`apps-source.patch` contains one module, ten synthetic tests and an existing
|
||||
RAPPS-WP-0016-T05 continuation. No canonical Apps source or status was edited,
|
||||
no commit was made and no owner assent was produced. NK-WP-0048-T34 remains open.
|
||||
|
||||
The module compares explicitly supplied public JSON projections against exact
|
||||
proposed acceptance digests. It permits differences only in the named
|
||||
`netkingdom` OAuth entry and `ENABLE_AUTO_REGISTRATION`/`ACCOUNT_LINKING` policy
|
||||
keys. It preserves other OAuth sources and all unrelated public settings,
|
||||
requires disabled registration and retained local sign-in, and rejects
|
||||
value-bearing key shapes. It pins the actual Makefile, public values, registry
|
||||
values and helper bytes. Output contains only metadata, hashes and command
|
||||
templates, never OAuth/group contents or helper output. The group reference
|
||||
bytes are exact acceptance-bound data; this does not establish chart key
|
||||
compatibility or the runtime any-of group behavior.
|
||||
|
||||
The upgrade template uses the actual Forgejo chart/version/namespace/release
|
||||
and private helper stream seam, excluding unrelated Make prerequisites and
|
||||
`--install`/namespace creation. The module imports no command/runtime API,
|
||||
opens no files, and has no dispatch runner or capability callbacks. The private
|
||||
helper is named but cannot execute. `activate()` always raises `Refused`, even
|
||||
when caller-supplied session, journal or exclusion flags are supplied. A source
|
||||
input is not an authenticated acceptance, observation or authority record.
|
||||
|
||||
Inverse preparation binds the exact transaction scope, immutable original
|
||||
before, confirmed post, current post equality and later restored revision with
|
||||
the original configuration hash. Pending, unknown and already-restored records
|
||||
require owner reconciliation rather than unguarded replay. Preexisting matched
|
||||
configuration yields no rollback command. All inverse templates remain
|
||||
non-dispatchable and require a fresh current inverse operation session; the
|
||||
revoked hand-over parent and stored provenance confer no permission. The
|
||||
separate control-flow candidate's `inverse_session` placement is unchanged.
|
||||
|
||||
The original receipt-only rollback seam and preexisting NOCHANGE behavior are
|
||||
untouched: this patch does not edit or replace any adapter or kernel step. It
|
||||
does not introduce an eighth step or switch configuration ownership to a
|
||||
Forgejo API mutation.
|
||||
|
||||
## Actual facilities selected for owner continuation
|
||||
|
||||
These are source implementation proposals within T05, not authority grants or
|
||||
an assertion that the missing implementations exist.
|
||||
|
||||
| Need | Selected existing owner surface | Exact remaining implementation |
|
||||
| --- | --- | --- |
|
||||
| Accepted case configuration | Apps `helm/forgejo-values.yaml`, registry values and existing `tools/forgejo-helm-secrets.sh`; chart 12.5.0 | Owner must publish the exact accepted public OAuth/group-reference projection and chart-compatible secret reference. A confidential projection must prove DB/admin/helper values unchanged without disclosing or emitting them. The preparer never consumes `helm get values` output. |
|
||||
| Authenticated durable configuration history | Existing owner choice of committed `docs/evidence/<date>-forgejo-netkingdom-handover.json` identified by path@commit, extended within the same repository evidence surface | Retain original intent before mutation; bind separate confirmed post, inverse attempt and verified outcome records to that immutable intent and transaction. Authenticate owner custody and main publication/readback. Stop on pending/publication ambiguity. A local file or git commit alone is not authenticated execution permission. No remote publication or new service is selected here. |
|
||||
| Current operation/inverse session | Existing attended Apps owner option C (`apps-consumer-handover`), using Warden's actual `Invocation`/exact-source evidence-verification seam where applicable | Implement the actual owner provenance verifier, current disposition and revocation source. Bind principal/owner/case/acceptance/source/cluster/namespace/release/client/descriptor/transaction and distinct inverse operation. Revoke each operation attempt on success/refusal/failure. The same operator may perform the fresh inverse; no new standing agent identity is proposed. |
|
||||
| Release-wide serialization | Existing Apps owner-operated release tooling capability, with attended release-freeze/reconciliation as the proposed mechanism | Owner must bind effective exclusion of manual Helm, Make and every external automation/reconciler across current-check, command and readback. No implemented enforcement was found. Helm has no CAS. Local locks, Warden journal locks and revision equality are insufficient; dispatch must stay disabled. |
|
||||
|
||||
Warden `case_run.Executor` explicitly injects an evidence verifier and fixed
|
||||
adapters; none ship by default. Its `_admission` rechecks current session
|
||||
evidence, but that does not implement a separate Apps configuration inverse
|
||||
session. `Journal` provides private SQLite, synchronous FULL and a process
|
||||
lock for its own case receipts/intents; it is neither this configuration journal
|
||||
nor exclusion of other Helm writers. Its code is inspected, not instantiated.
|
||||
The Apps private-reader runtime-proof wrapper authenticates a different read
|
||||
operation and is not reused as release-write or inverse permission.
|
||||
|
||||
## Bounded writer inventory
|
||||
|
||||
The archive's `Makefile`, all `tools/` source and all three workflow source
|
||||
files were searched, together with the Forgejo/operator runbooks. Paths/hashes
|
||||
are in `source-inventory.json`. No credentials/config from HOME were inspected.
|
||||
|
||||
The direct Forgejo Helm writer is `Makefile:483` (`forgejo-deploy`), with
|
||||
`Makefile:465` its server dry-run variant. The former depends on mailer ESO,
|
||||
OpenBao store/operator installation and hook-template deployment; those are
|
||||
outside this OAuth-only transaction. Runbook deployment commands invoke this
|
||||
Make target. No independent Forgejo release writer was found in the archived
|
||||
tools or workflows. Other release writers exist for inter-hub/core-hub/vergabe/
|
||||
coulomb/reuse; their configurable Make release variables and arbitrary manual
|
||||
Helm access cannot be assumed excluded from Forgejo. External writers and
|
||||
cluster reconciler permissions are not enumerable or enforceable from this
|
||||
bounded source inventory. There is no claim of exhaustive estate writer
|
||||
discovery or production writer freeze.
|
||||
|
||||
## Validation and limits
|
||||
|
||||
Ten tests pass under an empty HOME and allowlisted environment. The test code
|
||||
blocks subprocess creation and sockets before exercising preparation. Checks
|
||||
cover exact scope/source mismatch, unrelated release and OAuth policy changes,
|
||||
duplicate sources, unsafe registration, value-bearing keys, case-only templates,
|
||||
original-before/post/current/inverse guards, ambiguous states and preexisting
|
||||
NOCHANGE. No native tool, credential read, external network, browser/MFA or
|
||||
owner session was exercised. This proves offline validation mechanics only.
|
||||
|
||||
Synthetic public projections intentionally do not assert the chart's actual
|
||||
OAuth schema. No production accepted projection was available in the bounded
|
||||
owner source. Actual chart rendering, authoritative private/public readback,
|
||||
durable publication failure/recovery and all four consumer login/group probes
|
||||
remain real T02/T03/T05 work. The existing T05 workplan's needs-change history
|
||||
and task status remain retained in the source draft.
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"RAPPS-WP-0016-T05-source-draft.md": "faf5b7e38cdfd7778a6ff98d19f1ce7a3a163cd11ec321dff397e480ad99a8ad",
|
||||
"README.md": "3caaa38b16e9016fe0b2f7b478e87d383c5b1bef546282727cf14d695c05b8e4",
|
||||
"apps-source.patch": "f2363df928db278b0c5303e37efa53ed8397bc40725b240e69d78e3d0ad7e362",
|
||||
"source-inventory.json": "b57fc904ae6baf2d73cc5c5465ee5463590a966eebe3d30b91aaeb6e8432bcd2",
|
||||
"tests/test_forgejo_configuration_prepare.py": "7ca372cb5b72b8e16af4b3a277fd04613afdc90ebf77d38a223687b72fb91491",
|
||||
"tests.log": "2f9d4d78cae19f752436fec5ff770e3a80e81dae9ce5caa6e048efeb28668873",
|
||||
"tools/forgejo_configuration_prepare.py": "414ecf813db17ef1dd767626065e652b715c1db1f37f61b1f81303b0de7ca921",
|
||||
"validation.json": "acd2ef5226c7f04396676804b4de29e6db12682739659971e0040ca0cbb494a7"
|
||||
}
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
{
|
||||
"apps_head": "944b30ad63ab054cc24617e374e177d34cd572da",
|
||||
"scope": "bounded public source only; no secrets, runtime, native calls or network",
|
||||
"files": [
|
||||
{
|
||||
"path": ".forgejo/workflows/ci-smoke.yaml",
|
||||
"sha256": "c17729b2144648fa92871e1c94bcb6bd8f64a4b533577ede9457593758e2b7ab"
|
||||
},
|
||||
{
|
||||
"path": ".gitea/workflows/inter-hub-production-deploy.yaml",
|
||||
"sha256": "b8d7a8c7de94d449a9d1846f5336f606dc72cc0d3c64136be0e7e5feb48d61b9"
|
||||
},
|
||||
{
|
||||
"path": ".gitea/workflows/manifest-server-dry-run.yaml",
|
||||
"sha256": "2b0f0a536c8aec797705b4d7329eb748734597d8593a697e628a5cae693fe3fa"
|
||||
},
|
||||
{
|
||||
"path": "AGENTS.md",
|
||||
"sha256": "2a366a5fbb69ba235df97ee117153cbfa1af11ab579372751b4fb5dbb30c31a7"
|
||||
},
|
||||
{
|
||||
"path": "INTENT.md",
|
||||
"sha256": "31060dc1738aa4540f78735019359a276ece39dad8afbf1be740713bebbe65e5"
|
||||
},
|
||||
{
|
||||
"path": "Makefile",
|
||||
"sha256": "a319e264e6f04378f6b3d2c7781860fc4bac1abdb248cffa8c26065e08f2f290"
|
||||
},
|
||||
{
|
||||
"path": "docs/assessments/cust81-private-reader/phase-b-r1/native_runtime_proof_stdin.py",
|
||||
"sha256": "1edc317e8563a120b62a6830eebb2875c144b45c57a491eb7da849095c5d1933"
|
||||
},
|
||||
{
|
||||
"path": "docs/evidence/nk48-t16-owner-review/owner-review-railiance-apps.json",
|
||||
"sha256": "67e9ded8d37462ba04a0a224bac03a840d0c94326235474e1d3371c8ff61865d"
|
||||
},
|
||||
{
|
||||
"path": "docs/forgejo-on-railiance01.md",
|
||||
"sha256": "a71670f0241d4bb76ea457f2120a332d3a53ec3f3098859982fb0b283f97370e"
|
||||
},
|
||||
{
|
||||
"path": "docs/operator-recipes.md",
|
||||
"sha256": "d0c8c4b4ea0ea4d148153724e11e8396249d14609dc3095bdab3adec0132694e"
|
||||
},
|
||||
{
|
||||
"path": "docs/proposals/2026-10-11-forgejo-consumer-eso/owner-design.md",
|
||||
"sha256": "c523949454e4c6eb58e8caee382380a561f561704136de62bf5783c592f962f2"
|
||||
},
|
||||
{
|
||||
"path": "helm/forgejo-registry-values.yaml",
|
||||
"sha256": "a38f551734843c6367448f267c7376b1aeee010927a49690fdd7c1471e0ae450"
|
||||
},
|
||||
{
|
||||
"path": "helm/forgejo-values.yaml",
|
||||
"sha256": "cf87a3dccd666bf8870619755afac4af64fc0bee77b0f0ea54718d6384b70d4f"
|
||||
},
|
||||
{
|
||||
"path": "registry/capabilities/capability.railiance.workload-deployment-tooling.md",
|
||||
"sha256": "ad24b7656512f93698af3182a3187c2e32efd5e74ae69007cb86eb7cd889528f"
|
||||
},
|
||||
{
|
||||
"path": "tools/apps-pg-backup-dry-run.sh",
|
||||
"sha256": "a468d6c80fb13e4bc5e67acbd51f11ae08d2543fe1366e846e940e7356674160"
|
||||
},
|
||||
{
|
||||
"path": "tools/apps-pg-restore-drill.sh",
|
||||
"sha256": "ba94c3f56c54e6c899718ed3b89dc18308d2e334cfa68c04828df9f616ce9c3d"
|
||||
},
|
||||
{
|
||||
"path": "tools/build-database-url-secret.sh",
|
||||
"sha256": "5d11320093170dbf77d0d4a390714a04491126f85703d701374c797f686ea2bd"
|
||||
},
|
||||
{
|
||||
"path": "tools/check-backup-lane-auth.sh",
|
||||
"sha256": "1a0ee0d0d4646e92c654567a9c7304a80d4b33b4aadd513a326af0d8a2a68266"
|
||||
},
|
||||
{
|
||||
"path": "tools/check-oci-image.sh",
|
||||
"sha256": "064b2a84fb3adfb45bbd704c718e8f0cf2fe48965d78b08c7b8be629eefeb5fe"
|
||||
},
|
||||
{
|
||||
"path": "tools/check-policy-nexus-binding.py",
|
||||
"sha256": "0db04d2d88038bc8079c559c8406d825fc6966968e27a801feefefb263bcc244"
|
||||
},
|
||||
{
|
||||
"path": "tools/check-sops.sh",
|
||||
"sha256": "e6ddd6c4ef78a7608d94e5ca3b36a4d25816f6e0c04cd34bb9e08eee441f4d74"
|
||||
},
|
||||
{
|
||||
"path": "tools/check-tools.sh",
|
||||
"sha256": "50a94cd27c694731407e3ee10f44e64d20b5d44d9af21163eb00c8d685f1542d"
|
||||
},
|
||||
{
|
||||
"path": "tools/cnpg-backup-offsite-secret-apply.sh",
|
||||
"sha256": "18a24ea2206a0775a4764779c96c7f93a2c36130790721a78c1806b8b6ac510a"
|
||||
},
|
||||
{
|
||||
"path": "tools/cnpg-backup-status.sh",
|
||||
"sha256": "3e5b474fc81a394b6cf4e91434194f1df88e1958d15326bbfb1160a60c67bc8b"
|
||||
},
|
||||
{
|
||||
"path": "tools/cnpg-logical-backup.sh",
|
||||
"sha256": "1ccbb608827af036fbbd5c132284227fe8a651e75e3a1abf66eb54382599cb25"
|
||||
},
|
||||
{
|
||||
"path": "tools/core-hub-helm-adopt.sh",
|
||||
"sha256": "047d793b9e763b62980df4ab0d2cef5aa1b115a3d6510bac6d96cf6adf5856e8"
|
||||
},
|
||||
{
|
||||
"path": "tools/core-hub-smoke.sh",
|
||||
"sha256": "5492aa72c6ecf5e3c22ac6c2b57385c594f52b216c525c531aec18a870e2c84f"
|
||||
},
|
||||
{
|
||||
"path": "tools/create-coulomb-social-env-secret.sh",
|
||||
"sha256": "a741de560918b54166e818a8302338546dbcd93572624436c779cb0518003bc8"
|
||||
},
|
||||
{
|
||||
"path": "tools/forgejo-breakglass-seal.sh",
|
||||
"sha256": "aef89b19f1f82acfbd565e07580295dd66037af27f0f3c4703327a2fe77fac33"
|
||||
},
|
||||
{
|
||||
"path": "tools/forgejo-breakglass-verify.sh",
|
||||
"sha256": "e0144114468015730c415a503e906154313ee79acaeeba5c5ddf0a0b79f9b599"
|
||||
},
|
||||
{
|
||||
"path": "tools/forgejo-helm-secrets.sh",
|
||||
"sha256": "ebc5425af168763923db9b120c4ac59e2b15f6aae4c3481946d8ccbc9c62412e"
|
||||
},
|
||||
{
|
||||
"path": "tools/forgejo-npm-smoke.sh",
|
||||
"sha256": "eb13107d933d109302916c4663bf95f5f49e9d2d1cc7272f96e229e179b92f66"
|
||||
},
|
||||
{
|
||||
"path": "tools/forgejo-openbao-eso-token-apply.sh",
|
||||
"sha256": "2e37b355631ef1d0f8a5741276f7c17f8c99e2d2dcf855e4e4273b1105d2309a"
|
||||
},
|
||||
{
|
||||
"path": "tools/forgejo-operator-bootstrap.sh",
|
||||
"sha256": "77f27b016fd772d112c9f4ab1ff7dce569d28bb60291ad6ef04f5896c0465960"
|
||||
},
|
||||
{
|
||||
"path": "tools/forgejo-runner-registration-sops-bootstrap.sh",
|
||||
"sha256": "3b5242696616dbb169e6315e9a70c7fd942724427fa24a60d820b556425782e5"
|
||||
},
|
||||
{
|
||||
"path": "tools/forgejo-smoke.sh",
|
||||
"sha256": "f6146412cbe29f4abba9d81b3bc7a0652cf281b30a56fd7054e72f1574553869"
|
||||
},
|
||||
{
|
||||
"path": "tools/forgejo-verify.sh",
|
||||
"sha256": "799571da79db58be2e6eee533ec20765aa9961bb74ea3865d69f48273a86f242"
|
||||
},
|
||||
{
|
||||
"path": "tools/inter-hub-smoke.sh",
|
||||
"sha256": "068fa77a76bb3047e581398560d02d0bb517614abbb935655fe849082b4666ee"
|
||||
},
|
||||
{
|
||||
"path": "tools/k8s-server-dry-run.sh",
|
||||
"sha256": "a6ff8018a777fd4a95a34609ca3efa22e5af314710522ca6fc02e852be5bb439"
|
||||
},
|
||||
{
|
||||
"path": "tools/reuse-forgejo-webhook.sh",
|
||||
"sha256": "bf9dfe0be5ca3033e20495dbd8914e59cedc7092254c544c1436f6f4f850a041"
|
||||
},
|
||||
{
|
||||
"path": "tools/reuse-openbao-eso-token-apply.sh",
|
||||
"sha256": "0ea9cf2b3767dac5cd43d51e15af86bfee366cbd55a2f9bf0e8e8b71714052d2"
|
||||
},
|
||||
{
|
||||
"path": "tools/reuse-webhook-smoke.sh",
|
||||
"sha256": "be7b076970d02e357f2216935d257b44a880dec8005860003448ea8c69270a51"
|
||||
},
|
||||
{
|
||||
"path": "tools/smoke-service.sh",
|
||||
"sha256": "0de60ff9e32d820ea60e3b197715a4a99167f929b69516e0b632019c046ea1a4"
|
||||
}
|
||||
],
|
||||
"owner_runtime_backend_found": false,
|
||||
"all_writer_exclusion_found": false,
|
||||
"other_public_source": [
|
||||
{
|
||||
"repo": "ops-warden",
|
||||
"path": "src/warden/case_run.py",
|
||||
"sha256": "1bc73e30ac718fd75bbaff2a739495fc051aeaec73802ac71f0583cb2d53ab0a"
|
||||
},
|
||||
{
|
||||
"repo": "ops-warden",
|
||||
"path": "src/warden/case_receipt_state.py",
|
||||
"sha256": "a2cda7f187e77bd76d4d209505638de44a587fae0bc033ec75c23ee9ea9f73bc"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
"""Synthetic offline tests; never acquire owner credentials or execute native tools."""
|
||||
import copy
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import socket
|
||||
import subprocess
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
spec = importlib.util.spec_from_file_location('prepare', ROOT / 'tools/forgejo_configuration_prepare.py')
|
||||
m = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(m)
|
||||
|
||||
|
||||
class PreparationTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.guards = [patch.object(subprocess, 'Popen', side_effect=AssertionError('native call')),
|
||||
patch.object(socket, 'socket', side_effect=AssertionError('network call'))]
|
||||
for guard in self.guards:
|
||||
guard.start()
|
||||
self.addCleanup(guard.stop)
|
||||
self.binding = dict(principal='attended-owner', owner='railiance-apps', case='synthetic-case',
|
||||
acceptance='synthetic-only', source=m.BASE, cluster='railiance01',
|
||||
namespace='forgejo', release='forgejo', client='forgejo',
|
||||
handover_descriptor='synthetic-descriptor')
|
||||
self.before = {'image': {'tag': 'unchanged'}, 'gitea': {
|
||||
'oauth': [{'name': 'preexisting', 'provider': 'other'}], 'config': {
|
||||
'service': {'DISABLE_REGISTRATION': True},
|
||||
'oauth2_client': {'UNRELATED': 'keep'}}}}
|
||||
self.after = copy.deepcopy(self.before)
|
||||
self.after['gitea']['oauth'].append({'name': 'netkingdom', 'provider': 'openidConnect',
|
||||
'existingSecret': 'synthetic-client-reference',
|
||||
'groups': ['synthetic-access-group']})
|
||||
self.after['gitea']['config']['oauth2_client'].update(
|
||||
ENABLE_AUTO_REGISTRATION=False, ACCOUNT_LINKING='login')
|
||||
self.contents = {p: (ROOT / p).read_bytes() for p in m.SOURCE_PATHS}
|
||||
|
||||
def accepted(self, before=None, after=None):
|
||||
return {'binding_sha256': m.digest(self.binding),
|
||||
'before_sha256': m.digest(self.before if before is None else before),
|
||||
'after_sha256': m.digest(self.after if after is None else after),
|
||||
'source_sha256': m.SOURCE_HASHES.copy()}
|
||||
|
||||
def prepare(self, before=None, after=None):
|
||||
return m.prepare(self.binding, self.before if before is None else before,
|
||||
self.after if after is None else after,
|
||||
self.accepted(before, after), self.contents)
|
||||
|
||||
def observation(self, revision, config):
|
||||
return dict(binding_sha256=m.digest(self.binding), revision=revision,
|
||||
configuration_sha256=m.digest(config), status='deployed')
|
||||
|
||||
def transaction(self):
|
||||
return dict(id='synthetic-trial', binding_sha256=m.digest(self.binding),
|
||||
state='confirmed', changed=True,
|
||||
before=self.observation(7, self.before), after=self.observation(8, self.after))
|
||||
|
||||
def test_exact_projection_and_command_without_unrelated_prerequisites(self):
|
||||
result = self.prepare()
|
||||
self.assertFalse(result['dispatchable'])
|
||||
self.assertEqual(result['command_template'][:6],
|
||||
['helm', 'upgrade', 'forgejo', 'gitea-charts/gitea', '--version', '12.5.0'])
|
||||
self.assertNotIn('--install', result['command_template'])
|
||||
self.assertNotIn('make', result['command_template'])
|
||||
encoded = json.dumps(result)
|
||||
self.assertNotIn('synthetic-access-group', encoded)
|
||||
self.assertNotIn('synthetic-client-reference', encoded)
|
||||
|
||||
def test_unrelated_changes_even_if_proposed_acceptance_contains_them(self):
|
||||
candidates = []
|
||||
for path, value in [(('image', 'tag'), 'changed'),
|
||||
(('gitea', 'config', 'oauth2_client', 'UNRELATED'), 'changed'),
|
||||
(('gitea', 'config', 'service', 'ENABLE_INTERNAL_SIGNIN'), False)]:
|
||||
after = copy.deepcopy(self.after)
|
||||
parent = after
|
||||
for key in path[:-1]:
|
||||
parent = parent[key]
|
||||
parent[path[-1]] = value
|
||||
candidates.append(after)
|
||||
after = copy.deepcopy(self.after)
|
||||
after['gitea']['oauth'][0]['provider'] = 'changed'
|
||||
candidates.append(after)
|
||||
for after in candidates:
|
||||
with self.subTest(after=m.digest(after)), self.assertRaises(m.Refused):
|
||||
self.prepare(after=after)
|
||||
|
||||
def test_case_scope_source_and_acceptance_mismatch(self):
|
||||
for key in self.binding:
|
||||
accepted = self.accepted()
|
||||
original = self.binding[key]
|
||||
self.binding[key] = original + '-different'
|
||||
with self.subTest(key=key), self.assertRaises(m.Refused):
|
||||
m.prepare(self.binding, self.before, self.after, accepted, self.contents)
|
||||
self.binding[key] = original
|
||||
for path in m.SOURCE_PATHS:
|
||||
contents = self.contents.copy()
|
||||
contents[path] += b'changed'
|
||||
with self.subTest(path=path), self.assertRaises(m.Refused):
|
||||
m.prepare(self.binding, self.before, self.after, self.accepted(), contents)
|
||||
|
||||
def test_value_bearing_input_is_not_echoed(self):
|
||||
for key in ('clientSecret', 'password', 'token', 'cookie', 'secret'):
|
||||
after = copy.deepcopy(self.after)
|
||||
after['gitea']['oauth'][-1][key] = 'do-not-echo'
|
||||
with self.subTest(key=key), self.assertRaises(m.Refused) as caught:
|
||||
self.prepare(after=after)
|
||||
self.assertNotIn('do-not-echo', str(caught.exception))
|
||||
|
||||
def test_duplicate_auth_source_and_registration_refused(self):
|
||||
after = copy.deepcopy(self.after)
|
||||
after['gitea']['oauth'].append(after['gitea']['oauth'][-1].copy())
|
||||
with self.assertRaises(m.Refused):
|
||||
self.prepare(after=after)
|
||||
after = copy.deepcopy(self.after)
|
||||
after['gitea']['config']['oauth2_client']['ENABLE_AUTO_REGISTRATION'] = True
|
||||
with self.assertRaises(m.Refused):
|
||||
self.prepare(after=after)
|
||||
|
||||
def test_exact_preexisting_nochange_has_no_rollback(self):
|
||||
plan = self.prepare(before=self.after)
|
||||
self.assertFalse(plan['projection']['changed'])
|
||||
tx = self.transaction()
|
||||
tx.update(changed=False, before=tx['after'])
|
||||
inverse = m.prepare_inverse(self.binding, tx, tx['after'])
|
||||
self.assertEqual(inverse['action'], 'preserve-preexisting')
|
||||
self.assertEqual(inverse['command_template'], [])
|
||||
|
||||
def test_inverse_uses_original_revision_and_checks_current(self):
|
||||
tx = self.transaction()
|
||||
plan = m.prepare_inverse(self.binding, tx, tx['after'])
|
||||
self.assertEqual(plan['command_template'][:4], ['helm', 'rollback', 'forgejo', '7'])
|
||||
self.assertFalse(plan['dispatchable'])
|
||||
for current in (self.observation(9, self.after), self.observation(8, self.before)):
|
||||
with self.assertRaises(m.Refused):
|
||||
m.prepare_inverse(self.binding, tx, current)
|
||||
|
||||
def test_pending_or_ambiguous_transaction_requires_reconciliation(self):
|
||||
for state in ('pending', 'inverse-started', 'restored', 'publication-unknown'):
|
||||
tx = self.transaction()
|
||||
tx['state'] = state
|
||||
with self.subTest(state=state), self.assertRaises(m.Refused):
|
||||
m.prepare_inverse(self.binding, tx, tx['after'])
|
||||
for field in ('revision', 'configuration_sha256'):
|
||||
tx = self.transaction()
|
||||
tx['after'][field] = tx['before'][field]
|
||||
with self.subTest(field=field), self.assertRaises(m.Refused):
|
||||
m.prepare_inverse(self.binding, tx, tx['after'])
|
||||
|
||||
def test_inverse_readback_requires_original_config_and_new_revision(self):
|
||||
tx = self.transaction()
|
||||
self.assertIn('durable-owner-confirmation-required',
|
||||
m.verify_inverse_readback(tx, self.observation(9, self.before)))
|
||||
for restored in (self.observation(7, self.before), self.observation(9, self.after)):
|
||||
with self.assertRaises(m.Refused):
|
||||
m.verify_inverse_readback(tx, restored)
|
||||
|
||||
def test_no_fabricated_facility_can_activate(self):
|
||||
for operation in ('configure', 'inverse'):
|
||||
with self.assertRaises(m.Refused):
|
||||
m.activate(operation=operation, authenticated=True,
|
||||
journal={}, lock=True, inverse_session='synthetic')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"base": "944b30ad63ab054cc24617e374e177d34cd572da",
|
||||
"command": [
|
||||
"/usr/bin/python3",
|
||||
"-m",
|
||||
"unittest",
|
||||
"discover",
|
||||
"-s",
|
||||
"tests",
|
||||
"-p",
|
||||
"test_forgejo_configuration_prepare.py",
|
||||
"-v"
|
||||
],
|
||||
"cwd": "/tmp/apps-execution-backend-r1/source",
|
||||
"environment": {
|
||||
"HOME": "/tmp/apps-execution-backend-r1/empty-home",
|
||||
"PATH": "/usr/bin:/bin",
|
||||
"PYTHONDONTWRITEBYTECODE": "1"
|
||||
},
|
||||
"exit_code": 0,
|
||||
"tests_run": 10,
|
||||
"synthetic_only": true,
|
||||
"test_network_and_native_processes": "socket.socket and subprocess.Popen blocked inside each synthetic test",
|
||||
"native_or_credential_calls": false,
|
||||
"external_network": false,
|
||||
"operational_backend": false,
|
||||
"first_case_acceptance": false,
|
||||
"canonical_apps_edits": false,
|
||||
"existing_task": "RAPPS-WP-0016-T05",
|
||||
"existing_netkingdom_task": "NK-WP-0048-T34",
|
||||
"patch_sha256": "f2363df928db278b0c5303e37efa53ed8397bc40725b240e69d78e3d0ad7e362",
|
||||
"patch_apply_check": {
|
||||
"command": [
|
||||
"git",
|
||||
"apply",
|
||||
"--check",
|
||||
"/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-17/apps-execution-backend-r1/apps-source.patch"
|
||||
],
|
||||
"cwd": "/tmp/apps-execution-backend-r1/apply-check",
|
||||
"exit_code": 0,
|
||||
"baseline_workplan": "archived from frozen Apps commit; no canonical checkout touched"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
# Separate source proposal: private client-secret file, never secret argv
|
||||
|
||||
The reviewed seven-path extension and this release build do **not** add a new
|
||||
secret input mechanism. Current Forgejo 11.0.3's `secret` CLI flag has no private
|
||||
file/descriptor/environment input configured; the chart's automatic quoted
|
||||
`${GITEA_OAUTH_SECRET_<index>}` becomes a value-bearing `--secret` argument at
|
||||
runtime. There is no admitted existing alternate path in the bounded source.
|
||||
No actual secret or auth operation was used during this build.
|
||||
|
||||
A concrete owner source continuation is:
|
||||
|
||||
1. Add a distinct `--secret-file` StringFlag to Forgejo add/update OAuth CLI.
|
||||
Resolve it only inside the existing owner workload after operation admission.
|
||||
Reject simultaneous `--secret`/`--secret-file` and reject empty file paths.
|
||||
For update, omission preserves the existing stored secret, while an explicitly
|
||||
selected private input must succeed completely before any DB source update.
|
||||
Errors contain only fixed reasons, never secret bytes or serialized config.
|
||||
2. Read a bounded regular file with a non-following open and file-descriptor
|
||||
metadata checks; reject symlinks/FIFO/device/oversize/short or failed reads,
|
||||
ownership/mode mismatch, writable-by-others paths and empty values. Parent
|
||||
directories and the exact mount identity must be owner-controlled. Preserve
|
||||
exact secret bytes; do not trim or log them. The accepted secret encoding and
|
||||
maximum length must match the existing minted-client contract. Keep the
|
||||
transient string out of command tracing, diagnostics and public receipts.
|
||||
The process already holds the secret for the existing DB configuration; this
|
||||
proposal changes the input boundary, not its custody authority.
|
||||
3. Use the existing `forgejo/forgejo-netkingdom-oidc-client` Secret,
|
||||
key `CLIENT_SECRET`, through a **reviewed read-only file subPath mount** into
|
||||
the existing configure init container. A file subPath avoids the projected
|
||||
Secret symlink leaf and permits non-following file checks. The exact mount,
|
||||
UID/GID/mode and allowed reader set require owner/runtime admission; no file
|
||||
has been mounted or permission asserted here. Do not add an alias key or a
|
||||
new remote read/ACL. Rotation/restart behavior of a subPath mount needs the
|
||||
existing owner refresh contract; it does not update in place.
|
||||
4. The current chart helper would automatically add `--secret` even when a new
|
||||
`secretFile` key is supplied. Therefore a **separately reviewed chart-source
|
||||
patch** is also required. In chart 12.5.0 `_helpers.tpl`'s
|
||||
`gitea.oauth_settings`, change the default-secret insertion condition from
|
||||
`if not (hasKey $values "secret")` to
|
||||
`if and (not (hasKey $values "secret")) (not (hasKey $values "secretFile"))`.
|
||||
Reject both keys together. The existing generic key formatter then emits the
|
||||
implemented `--secret-file` flag containing only the fixed private path.
|
||||
Omit `existingSecret`, the indexed secret environment entry and the legacy
|
||||
`secret` key for this case. Do not put the secret itself in any public value,
|
||||
Make variable, agent argument, Helm override or receipt.
|
||||
|
||||
This is a source proposal, not an implemented/accepted flag or chart feature.
|
||||
It must be implemented and independently tested as a new exact source revision,
|
||||
with CLI add/update preservation, source serialization unchanged except intended
|
||||
secret replacement, mutual exclusion, symlink/permission/file-race/error cases,
|
||||
no argv/env/log/receipt value leakage and actual chart command rendering. A
|
||||
secret-file path flag alone is insufficient if the helper still injects
|
||||
`--secret`. The source/chart archives and owner descriptors must then be
|
||||
reviewed/re-frozen. Existing type 1.3 does not admit this mechanism implicitly.
|
||||
|
||||
The agent can prepare public source, references and readback schema; the actual
|
||||
workload reads the value only after the authenticated access-engine decision,
|
||||
current operation session and private-runtime admission. No standing agent
|
||||
credential or value-bearing agent argv is proposed. The T34 owner backend still
|
||||
owes exact application before/post readback, durable journal, all-writer
|
||||
exclusion, current operation/inverse sessions and the application-state inverse.
|
||||
This source proposal supplies none of those grants or implementations.
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"argv": [
|
||||
"docker",
|
||||
"buildx",
|
||||
"build",
|
||||
"--load",
|
||||
"--platform",
|
||||
"linux/amd64",
|
||||
"--pull=false",
|
||||
"--network=none",
|
||||
"--provenance=false",
|
||||
"--sbom=false",
|
||||
"--iidfile",
|
||||
"/tmp/apps-forgejo-release-repro-r1/image-id",
|
||||
"--metadata-file",
|
||||
"/tmp/apps-forgejo-release-repro-r1/image-build-metadata.json",
|
||||
"--build-arg",
|
||||
"SOURCE_DATE_EPOCH=1752105237",
|
||||
"/tmp/apps-forgejo-release-repro-r1/container-context"
|
||||
],
|
||||
"phase": "container-build",
|
||||
"returncode": 0,
|
||||
"seconds": 13.55
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"argv": [
|
||||
"docker",
|
||||
"image",
|
||||
"save",
|
||||
"--output",
|
||||
"/tmp/apps-forgejo-release-repro-r1/image.tar",
|
||||
"sha256:ffae6c666403ab2f995dc017069bcb1a9aea9c6e9d21a55b2ae5a7df5d71ba64"
|
||||
],
|
||||
"phase": "container-save",
|
||||
"returncode": 1,
|
||||
"seconds": 0.05
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
{
|
||||
"argv": [
|
||||
"bwrap",
|
||||
"--unshare-net",
|
||||
"--unshare-pid",
|
||||
"--proc",
|
||||
"/proc",
|
||||
"--dev",
|
||||
"/dev",
|
||||
"--tmpfs",
|
||||
"/tmp",
|
||||
"--dir",
|
||||
"/app",
|
||||
"--ro-bind",
|
||||
"/tmp/apps-forgejo-release-repro-r1/source/gitea",
|
||||
"/app/forgejo",
|
||||
"--bind",
|
||||
"/tmp/apps-forgejo-release-repro-r1/embedded-extracted",
|
||||
"/results",
|
||||
"--chdir",
|
||||
"/tmp",
|
||||
"--clearenv",
|
||||
"--setenv",
|
||||
"PATH",
|
||||
"/nonexistent",
|
||||
"--setenv",
|
||||
"HOME",
|
||||
"/tmp/home",
|
||||
"--setenv",
|
||||
"USER",
|
||||
"builder",
|
||||
"--setenv",
|
||||
"USERNAME",
|
||||
"builder",
|
||||
"--setenv",
|
||||
"GITEA_WORK_DIR",
|
||||
"/tmp/work",
|
||||
"--setenv",
|
||||
"GITEA_CUSTOM",
|
||||
"/tmp/custom",
|
||||
"--setenv",
|
||||
"GITEA_APP_INI",
|
||||
"/tmp/absent-app.ini",
|
||||
"--setenv",
|
||||
"TZ",
|
||||
"UTC",
|
||||
"--setenv",
|
||||
"LC_ALL",
|
||||
"C",
|
||||
"--",
|
||||
"/app/forgejo",
|
||||
"embedded",
|
||||
"extract",
|
||||
"--include-vendored",
|
||||
"--destination",
|
||||
"/results"
|
||||
],
|
||||
"phase": "embedded-extract",
|
||||
"returncode": 1,
|
||||
"seconds": 0.15
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
# Build reproducibility
|
||||
|
||||
## Reproducing the pinned dependencies
|
||||
|
||||
Reproducing the pinned dependencies is supported since BuildKit v0.11.
|
||||
|
||||
e.g.,
|
||||
```bash
|
||||
buildctl build --frontend dockerfile.v0 --local dockerfile=. --local context=. --source-policy-file policy.json
|
||||
```
|
||||
|
||||
An example `policy.json`:
|
||||
```json
|
||||
{
|
||||
"rules": [
|
||||
{
|
||||
"action": "CONVERT",
|
||||
"selector": {
|
||||
"identifier": "docker-image://docker.io/library/alpine:latest"
|
||||
},
|
||||
"updates": {
|
||||
"identifier": "docker-image://docker.io/library/alpine:latest@sha256:4edbd2beb5f78b1014028f4fbb99f3237d9561100b6881aabbf5acce2c4f9454"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "CONVERT",
|
||||
"selector": {
|
||||
"identifier": "https://raw.githubusercontent.com/moby/buildkit/v0.10.1/README.md"
|
||||
},
|
||||
"updates": {
|
||||
"attrs": {"http.checksum": "sha256:6e4b94fc270e708e1068be28bd3551dc6917a4fc5a61293d51bb36e6b75c4b53"}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Any source type is supported, but how to pin a source depends on the type.
|
||||
|
||||
## `SOURCE_DATE_EPOCH`
|
||||
[`SOURCE_DATE_EPOCH`](https://reproducible-builds.org/docs/source-date-epoch/) is the convention for pinning timestamps to a specific value.
|
||||
|
||||
The Dockerfile frontend supports consuming the `SOURCE_DATE_EPOCH` value as a special build arg, since BuildKit 0.11.
|
||||
Minimal support is also available on older BuildKit when using Dockerfile 1.5 frontend.
|
||||
|
||||
```console
|
||||
buildctl build --frontend dockerfile.v0 --opt build-arg:SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct) ...
|
||||
```
|
||||
The `buildctl` CLI (>= v0.13) and Docker Buildx (>= 0.10) automatically propagate the `$SOURCE_DATE_EPOCH` environment value from the client host to the `SOURCE_DATE_EPOCH` build arg.
|
||||
|
||||
The build arg value is used for:
|
||||
- the `created` timestamp in the [OCI Image Config](https://github.com/opencontainers/image-spec/blob/main/config.md#properties)
|
||||
- the `created` timestamp in the `history` objects in the [OCI Image Config](https://github.com/opencontainers/image-spec/blob/main/config.md#properties)
|
||||
- the `org.opencontainers.image.created` annotation in the [OCI Image Index](https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys)
|
||||
- the timestamp of the files exported with the `local` exporter
|
||||
- the timestamp of the files exported with the `tar` exporter
|
||||
|
||||
To apply the build arg value to the timestamps of the files inside the image, specify `rewrite-timestamp=true` as an image exporter option:
|
||||
```
|
||||
--output type=image,name=docker.io/username/image,push=true,rewrite-timestamp=true
|
||||
```
|
||||
|
||||
The `rewrite-timestamp` option is available since BuildKit v0.13.
|
||||
See [v0.12 documentation](https://github.com/moby/buildkit/blob/v0.12/docs/build-repro.md#caveats) for dealing with timestamps
|
||||
in BuildKit v0.12 and v0.11.
|
||||
|
||||
See also the [documentation](/frontend/dockerfile/docs/reference.md#buildkit-built-in-build-args) of the Dockerfile frontend.
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"url": "https://raw.githubusercontent.com/moby/buildkit/v0.27.1/docs/build-repro.md",
|
||||
"sha256": "1e503e37e994311600745f61ed0173a7ae4217ce307cf72513230e534f5833ca",
|
||||
"primary_source": true
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"argv": [
|
||||
"docker",
|
||||
"buildx",
|
||||
"build",
|
||||
"--output",
|
||||
"type=oci,dest=/tmp/apps-forgejo-release-repro-r1/image.oci.tar,rewrite-timestamp=true",
|
||||
"--no-cache",
|
||||
"--platform",
|
||||
"linux/amd64",
|
||||
"--pull=false",
|
||||
"--network=none",
|
||||
"--provenance=false",
|
||||
"--sbom=false",
|
||||
"--metadata-file",
|
||||
"/tmp/apps-forgejo-release-repro-r1/image-build-metadata.json",
|
||||
"--build-arg",
|
||||
"SOURCE_DATE_EPOCH=1752120742",
|
||||
"/tmp/apps-forgejo-release-repro-r1/container-context"
|
||||
],
|
||||
"phase": "container-build",
|
||||
"returncode": 0,
|
||||
"seconds": 22.18
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"source_epoch": 1752105237, "base_created": "2025-07-10T04:12:21.034425519Z", "container_epoch": 1752120742, "derivation": "max(source epoch,ceil immutable base created timestamp)"}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"image_build_succeeded": true,
|
||||
"failed_phase": "image export only",
|
||||
"reason": "Docker29 containerd store addresses untagged image by manifest digest, not iid config digest",
|
||||
"config_digest": "sha256:ffae6c666403ab2f995dc017069bcb1a9aea9c6e9d21a55b2ae5a7df5d71ba64",
|
||||
"manifest_digest": "sha256:7cee81d2fce0b5a189de9702f3e800684dd4155176110469a911f0d7ac188f10",
|
||||
"resume_exact_existing_image_no_rebuild": true,
|
||||
"same_trial": true
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
{
|
||||
"source_binary_bytes_identical": true,
|
||||
"layer_headers": {
|
||||
"image.tar": [
|
||||
{
|
||||
"path": "app",
|
||||
"mtime": 1752120740,
|
||||
"uid": 0,
|
||||
"gid": 0,
|
||||
"mode": 493
|
||||
},
|
||||
{
|
||||
"path": "app/gitea",
|
||||
"mtime": 1792273215,
|
||||
"uid": 0,
|
||||
"gid": 0,
|
||||
"mode": 493
|
||||
},
|
||||
{
|
||||
"path": "app/gitea/gitea",
|
||||
"mtime": 1752105237,
|
||||
"uid": 0,
|
||||
"gid": 0,
|
||||
"mode": 493
|
||||
}
|
||||
],
|
||||
"image-repeat.tar": [
|
||||
{
|
||||
"path": "app",
|
||||
"mtime": 1752120740,
|
||||
"uid": 0,
|
||||
"gid": 0,
|
||||
"mode": 493
|
||||
},
|
||||
{
|
||||
"path": "app/gitea",
|
||||
"mtime": 1792273359,
|
||||
"uid": 0,
|
||||
"gid": 0,
|
||||
"mode": 493
|
||||
},
|
||||
{
|
||||
"path": "app/gitea/gitea",
|
||||
"mtime": 1752105237,
|
||||
"uid": 0,
|
||||
"gid": 0,
|
||||
"mode": 493
|
||||
}
|
||||
]
|
||||
},
|
||||
"finding": "app/gitea parent directory mtime changes on COPY despite fixed input file mtime; explicit layer timestamp rewriting needed"
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"no_cache_repeat": true,
|
||||
"separate_context_from_independent_binary": true,
|
||||
"returncode": 0,
|
||||
"seconds": 11.14,
|
||||
"identical_output_digests": false,
|
||||
"digests": {
|
||||
"containerimage.digest": "sha256:c24e0097f53f31308312ca2f8e411869a36860a7e54c24587e4cde346bbbd1f2",
|
||||
"containerimage.config.digest": "sha256:f91ba4f17fbac1f563ccaef4f30aa4d9e2f59a950f75df388d78b57f73b3bf1f"
|
||||
},
|
||||
"registry_publication": false
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"all_base_layers_and_rootless_runtime_config_preserved": true,
|
||||
"all_oci_blob_digests_verified": true,
|
||||
"archive_sha256": "e0e74653321383e133485191f0ac5af53aafb065fb5809e20ede2808cec91539",
|
||||
"artifact_path": "/tmp/apps-forgejo-release-repro-r1/image.oci.tar",
|
||||
"config_digest": "sha256:e9e81da43c5ea6895a95b6304337a40874e3275833be5f21fc6ffc104f79a2af",
|
||||
"container_epoch": 1752120742,
|
||||
"deployment_tag": null,
|
||||
"final_layer_binary_exact": true,
|
||||
"host_or_secret_mounts": false,
|
||||
"image_manifest_digest": "sha256:9d12779f0936b9ccdbde402a9869eaad8bb91ba724d579b6c2983e8e58dabf96",
|
||||
"local_runtime_image_loaded": false,
|
||||
"oci_index_sha256": "24bd0e22e815dbc3470cad5a9dda509e68ad6bc751f2bfae85d02d95f8bc8cf5",
|
||||
"privileged_containers": false,
|
||||
"registry_publication": false,
|
||||
"source_epoch": 1752105237
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"argv": [
|
||||
"docker",
|
||||
"buildx",
|
||||
"build",
|
||||
"--load",
|
||||
"--platform",
|
||||
"linux/amd64",
|
||||
"--pull=false",
|
||||
"--network=none",
|
||||
"--provenance=false",
|
||||
"--sbom=false",
|
||||
"--iidfile",
|
||||
"/tmp/apps-forgejo-release-repro-r1/image-id",
|
||||
"--metadata-file",
|
||||
"/tmp/apps-forgejo-release-repro-r1/image-build-metadata.json",
|
||||
"--build-arg",
|
||||
"SOURCE_DATE_EPOCH=1752105237",
|
||||
"/tmp/apps-forgejo-release-repro-r1/container-context"
|
||||
],
|
||||
"phase": "container-build",
|
||||
"returncode": 0,
|
||||
"seconds": 13.55
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"all_base_layers_and_rootless_runtime_config_preserved": true,
|
||||
"archive_sha256": "dca200a1aae8c62c4003e9d1817548b4c6dea11cee19a7687e92dbca4859d9f9",
|
||||
"config_digest": "sha256:ffae6c666403ab2f995dc017069bcb1a9aea9c6e9d21a55b2ae5a7df5d71ba64",
|
||||
"deployment_tag": null,
|
||||
"final_layer_binary_exact": true,
|
||||
"host_or_secret_mounts": false,
|
||||
"local_export_reference": "sha256:7cee81d2fce0b5a189de9702f3e800684dd4155176110469a911f0d7ac188f10",
|
||||
"local_image_id": "sha256:7cee81d2fce0b5a189de9702f3e800684dd4155176110469a911f0d7ac188f10",
|
||||
"privileged_containers": false,
|
||||
"registry_publication": false
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
{
|
||||
"buildx.build.provenance": {
|
||||
"builder": {
|
||||
"id": ""
|
||||
},
|
||||
"buildType": "https://mobyproject.org/buildkit@v1",
|
||||
"materials": [
|
||||
{
|
||||
"uri": "pkg:docker/code.forgejo.org/forgejo/forgejo?digest=sha256:41649dff5311664097d29b4d80620403f0b27e7c0eee0b8af79b132a37f3807b\u0026platform=linux%2Famd64",
|
||||
"digest": {
|
||||
"sha256": "41649dff5311664097d29b4d80620403f0b27e7c0eee0b8af79b132a37f3807b"
|
||||
}
|
||||
}
|
||||
],
|
||||
"invocation": {
|
||||
"configSource": {
|
||||
"entryPoint": "Dockerfile"
|
||||
},
|
||||
"parameters": {
|
||||
"frontend": "dockerfile.v0",
|
||||
"args": {
|
||||
"build-arg:SOURCE_DATE_EPOCH": "1752105237",
|
||||
"force-network-mode": "none"
|
||||
},
|
||||
"locals": [
|
||||
{
|
||||
"name": "context"
|
||||
},
|
||||
{
|
||||
"name": "dockerfile"
|
||||
}
|
||||
]
|
||||
},
|
||||
"environment": {
|
||||
"platform": "linux/amd64"
|
||||
}
|
||||
}
|
||||
},
|
||||
"buildx.build.ref": "default/default/uhbf0ilfuaklbkxp6x34k9ci4",
|
||||
"containerimage.config.digest": "sha256:ffae6c666403ab2f995dc017069bcb1a9aea9c6e9d21a55b2ae5a7df5d71ba64",
|
||||
"containerimage.descriptor": {
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"digest": "sha256:7cee81d2fce0b5a189de9702f3e800684dd4155176110469a911f0d7ac188f10",
|
||||
"size": 2850,
|
||||
"platform": {
|
||||
"architecture": "amd64",
|
||||
"os": "linux"
|
||||
}
|
||||
},
|
||||
"containerimage.digest": "sha256:7cee81d2fce0b5a189de9702f3e800684dd4155176110469a911f0d7ac188f10",
|
||||
"image.name": "moby-dangling@sha256:7cee81d2fce0b5a189de9702f3e800684dd4155176110469a911f0d7ac188f10"
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"initial_observation": "Frozen original source-inventory hash did not match current public metadata; retained as an adverse comparison, not source drift.",
|
||||
"root_provenance_commit": "16236ef",
|
||||
"resolution": "Exact per-record reconstruction verifies original bytes. Only public identifier notation changed; frozen SHA map unmodified.",
|
||||
"original_sha256": "77aabbe413aae6ac31c342304afa200d09ff3c74e3fc9f18d2af6919a816ab94",
|
||||
"current_sha256": "866641169b73aa6e56d4f8f3d497eda28229bdf2aafe98daf5fefd09258338a1",
|
||||
"reconstructor_sha256": "ec20b3378ece29dcf7faf4ad5e610e1e85bfa260121f2b8c537665f9530466e0",
|
||||
"published_provenance_sha256": "770a5831999df8a8dc1b66dc65b738f58bedd4eb0dab56127f8d37b2b10107b2",
|
||||
"reconstruction_verified": true
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"argv": [
|
||||
"make",
|
||||
"GOFLAGS=-trimpath -buildvcs=false -p=4",
|
||||
"RELEASE_VERSION=11.0.3+nk48.f2788b3f",
|
||||
"FORGEJO_VERSION=11.0.3+nk48.f2788b3f",
|
||||
"FORGEJO_VERSION_API=11.0.3+nk48.f2788b3f",
|
||||
"TAGS=bindata timetzdata sqlite sqlite_unlock_notify",
|
||||
"go-check",
|
||||
"generate-backend",
|
||||
"static-executable"
|
||||
],
|
||||
"phase": "embedded-and-static-backend",
|
||||
"returncode": 0,
|
||||
"seconds": 288.85
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"controls": [
|
||||
{
|
||||
"case": "tampered-source",
|
||||
"refused": true,
|
||||
"exitcode": 1
|
||||
},
|
||||
{
|
||||
"case": "existing-workspace",
|
||||
"refused": true,
|
||||
"exitcode": 1
|
||||
}
|
||||
],
|
||||
"tampered_source_workspace_not_created": true,
|
||||
"existing_trial_not_overwritten": true
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"initial_patch_harness": "Python difflib omitted no-newline marker for immutable base-config JSON; git apply refused. Corrected generator emits standard marker without changing postimage bytes.",
|
||||
"second_harness": "Dry-run assertion expected python3 prefix but owner target correctly uses absolute /usr/bin/python3 -I. Assertion corrected to exact emitted command.",
|
||||
"result": "Final patch git apply --check and isolated apply pass; all six owner postimages exact; make dry run emits exactly two isolated build/container commands.",
|
||||
"embedded_patch_whitespace": "git apply warned about literal context whitespace inside newly added immutable patch file; --whitespace=nowarn preserves byte-exact reviewed patch. No whitespace fix or source change.",
|
||||
"build_restart": false
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
{
|
||||
"argv": [
|
||||
"bwrap",
|
||||
"--unshare-net",
|
||||
"--ro-bind",
|
||||
"/usr",
|
||||
"/usr",
|
||||
"--ro-bind",
|
||||
"/lib",
|
||||
"/lib",
|
||||
"--ro-bind",
|
||||
"/lib64",
|
||||
"/lib64",
|
||||
"--symlink",
|
||||
"usr/bin",
|
||||
"/bin",
|
||||
"--proc",
|
||||
"/proc",
|
||||
"--dev",
|
||||
"/dev",
|
||||
"--tmpfs",
|
||||
"/tmp",
|
||||
"--bind",
|
||||
"/tmp/apps-forgejo-release-build-r1",
|
||||
"/tmp/apps-forgejo-release-build-r1",
|
||||
"--bind",
|
||||
"/tmp/apps-forgejo-release-repro-r1/gocache",
|
||||
"/tmp/apps-forgejo-release-repro-r1/gocache",
|
||||
"--ro-bind",
|
||||
"/tmp/forgejo-independent-r1/go",
|
||||
"/tmp/forgejo-independent-r1/go",
|
||||
"--ro-bind",
|
||||
"/tmp/forgejo-independent-r1/gomodcache",
|
||||
"/tmp/forgejo-independent-r1/gomodcache",
|
||||
"--chdir",
|
||||
"/tmp/apps-forgejo-release-build-r1/source",
|
||||
"--",
|
||||
"env",
|
||||
"-i",
|
||||
"PATH=/tmp/apps-forgejo-release-build-r1/node-v22.15.0-linux-x64/bin:/tmp/forgejo-independent-r1/go/bin:/usr/bin:/bin",
|
||||
"HOME=/tmp/apps-forgejo-release-build-r1/home",
|
||||
"NPM_CONFIG_USERCONFIG=/tmp/apps-forgejo-release-build-r1/empty.npmrc",
|
||||
"NPM_CONFIG_GLOBALCONFIG=/tmp/apps-forgejo-release-build-r1/empty-global.npmrc",
|
||||
"NPM_CONFIG_CACHE=/tmp/apps-forgejo-release-build-r1/npm-cache",
|
||||
"NPM_CONFIG_REGISTRY=https://registry.npmjs.org",
|
||||
"NPM_CONFIG_AUDIT=false",
|
||||
"NPM_CONFIG_FUND=false",
|
||||
"NPM_CONFIG_UPDATE_NOTIFIER=false",
|
||||
"PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1",
|
||||
"GOTOOLCHAIN=local",
|
||||
"GOPROXY=off",
|
||||
"GOSUMDB=off",
|
||||
"GOVCS=*:off",
|
||||
"GOPATH=/tmp/apps-forgejo-release-build-r1/gopath",
|
||||
"GOMODCACHE=/tmp/forgejo-independent-r1/gomodcache",
|
||||
"GOCACHE=/tmp/apps-forgejo-release-repro-r1/gocache",
|
||||
"CGO_ENABLED=1",
|
||||
"GOMAXPROCS=4",
|
||||
"GOFLAGS=-trimpath -buildvcs=false -p=4",
|
||||
"SOURCE_DATE_EPOCH=1752105237",
|
||||
"TZ=UTC",
|
||||
"LC_ALL=C",
|
||||
"NPM_CONFIG_OFFLINE=true",
|
||||
"NODE_OPTIONS=--max-old-space-size=4096",
|
||||
"GIT_CONFIG_NOSYSTEM=1",
|
||||
"GIT_CONFIG_GLOBAL=/dev/null",
|
||||
"GIT_TERMINAL_PROMPT=0",
|
||||
"make",
|
||||
"GOFLAGS=-trimpath -buildvcs=false -p=4",
|
||||
"RELEASE_VERSION=11.0.3+nk48.f2788b3f",
|
||||
"FORGEJO_VERSION=11.0.3+nk48.f2788b3f",
|
||||
"FORGEJO_VERSION_API=11.0.3+nk48.f2788b3f",
|
||||
"TAGS=bindata timetzdata sqlite sqlite_unlock_notify",
|
||||
"go-check",
|
||||
"generate-backend",
|
||||
"static-executable"
|
||||
],
|
||||
"phase": "normalized-embedded-and-static-backend",
|
||||
"returncode": 0,
|
||||
"seconds": 17.39
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"binary_sha256": "4df429454b0289c6df76d7fd95d41394ebc286191809349b2c1e655b6be494e4",
|
||||
"explicit_make_GOFLAGS": true,
|
||||
"file_and_directory_times_normalized": true,
|
||||
"owner_approval": false,
|
||||
"predecessor_sha256": "bc34e12a8794e556764ab2325c31615f6ddbcc7a314aabf106b580e470189911",
|
||||
"refinement_same_trial": true,
|
||||
"source_locks_preserved": true
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"two_independent_workspace_frontend_builds": true,
|
||||
"asset_files": 628,
|
||||
"byte_identical": true,
|
||||
"different_paths": []
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"argv": [
|
||||
"make",
|
||||
"GOFLAGS=-trimpath -buildvcs=false -p=4",
|
||||
"RELEASE_VERSION=11.0.3+nk48.f2788b3f",
|
||||
"FORGEJO_VERSION=11.0.3+nk48.f2788b3f",
|
||||
"FORGEJO_VERSION_API=11.0.3+nk48.f2788b3f",
|
||||
"frontend"
|
||||
],
|
||||
"phase": "frontend",
|
||||
"returncode": 0,
|
||||
"seconds": 108.61
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
{
|
||||
"buildx.build.provenance": {
|
||||
"builder": {
|
||||
"id": ""
|
||||
},
|
||||
"buildType": "https://mobyproject.org/buildkit@v1",
|
||||
"materials": [
|
||||
{
|
||||
"uri": "pkg:docker/code.forgejo.org/forgejo/forgejo?digest=sha256:41649dff5311664097d29b4d80620403f0b27e7c0eee0b8af79b132a37f3807b\u0026platform=linux%2Famd64",
|
||||
"digest": {
|
||||
"sha256": "41649dff5311664097d29b4d80620403f0b27e7c0eee0b8af79b132a37f3807b"
|
||||
}
|
||||
}
|
||||
],
|
||||
"invocation": {
|
||||
"configSource": {
|
||||
"entryPoint": "Dockerfile"
|
||||
},
|
||||
"parameters": {
|
||||
"frontend": "dockerfile.v0",
|
||||
"args": {
|
||||
"build-arg:SOURCE_DATE_EPOCH": "1752120742",
|
||||
"force-network-mode": "none",
|
||||
"no-cache": ""
|
||||
},
|
||||
"locals": [
|
||||
{
|
||||
"name": "context"
|
||||
},
|
||||
{
|
||||
"name": "dockerfile"
|
||||
}
|
||||
]
|
||||
},
|
||||
"environment": {
|
||||
"platform": "linux/amd64"
|
||||
}
|
||||
}
|
||||
},
|
||||
"buildx.build.ref": "default/default/n1p4kafj9m6flpod8a9dbtxkm",
|
||||
"containerimage.config.digest": "sha256:e9e81da43c5ea6895a95b6304337a40874e3275833be5f21fc6ffc104f79a2af",
|
||||
"containerimage.descriptor": {
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"digest": "sha256:9d12779f0936b9ccdbde402a9869eaad8bb91ba724d579b6c2983e8e58dabf96",
|
||||
"size": 2841,
|
||||
"annotations": {
|
||||
"org.opencontainers.image.created": "2025-07-10T04:12:22Z"
|
||||
},
|
||||
"platform": {
|
||||
"architecture": "amd64",
|
||||
"os": "linux"
|
||||
}
|
||||
},
|
||||
"containerimage.digest": "sha256:9d12779f0936b9ccdbde402a9869eaad8bb91ba724d579b6c2983e8e58dabf96"
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
/usr/bin/python3 -I tools/forgejo-release-build.py build --workspace "/tmp/railiance-apps-forgejo-release"
|
||||
/usr/bin/python3 -I tools/forgejo-release-build.py container --workspace "/tmp/railiance-apps-forgejo-release"
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
049718cc0ce4f58de90f7b752b82a78868a21fe7685af1c603ba1d22ae8a4d67 node-v22.15.0-aix-ppc64.tar.gz
|
||||
1013d381e8a4ebe64484eed07db2637a570090fdef87924bf0beb92d9ba7cc92 node-v22.15.0-arm64.msi
|
||||
92eb58f54d172ed9dee320b8450f1390db629d4262c936d5c074b25a110fed02 node-v22.15.0-darwin-arm64.tar.gz
|
||||
6e278a107d50da24b644dd26810a639a5f8ca67b55086e6b693caabcbb759912 node-v22.15.0-darwin-arm64.tar.xz
|
||||
f7f42bee60d602783d3a842f0a02a2ecd9cb9d7f6f3088686c79295b0222facf node-v22.15.0-darwin-x64.tar.gz
|
||||
7dab3f93551d88f1e63db6b32bae6d4858e16740e9849ebbeac1d43f5055d8f0 node-v22.15.0-darwin-x64.tar.xz
|
||||
cda8bbbfb4f7fb19b65efd5faabc97ccea1e94422e7066b9a1e70280c1ca6453 node-v22.15.0-headers.tar.gz
|
||||
1f0377a9f3dbb85959d38a6ebc6e7e189f520f509488c401be4970d8718a14e9 node-v22.15.0-headers.tar.xz
|
||||
c3582722db988ed1eaefd590b877b86aaace65f68746726c1f8c79d26e5cc7de node-v22.15.0-linux-arm64.tar.gz
|
||||
d68adf72c531f1118bee75b20ffbc5911accfda5e73454a798625464b40a4adf node-v22.15.0-linux-arm64.tar.xz
|
||||
639a3ee3217049ba20e6f05651c7281a9d007111196387eb177081af8851e52f node-v22.15.0-linux-armv7l.tar.gz
|
||||
998da599863b8104992472ca4e738842b5e9b76b3665ea8bc13bd1ca52019426 node-v22.15.0-linux-armv7l.tar.xz
|
||||
a744107732546a3b112630cde1cc2db681559668cea36bee851348ed036c101c node-v22.15.0-linux-ppc64le.tar.gz
|
||||
e12384dd6d8e80ee1cdb8cf0f38545d3cc2a5b99f7dd2c6ae05067222d39ff7a node-v22.15.0-linux-ppc64le.tar.xz
|
||||
600afa22128e5d1b7d472109b8f9f93ace161dab3c1a8a642895675471916d86 node-v22.15.0-linux-s390x.tar.gz
|
||||
308e14b2b20445e03fbec920921b6b7f579832516b3e826b0d67be8acf0ac6f6 node-v22.15.0-linux-s390x.tar.xz
|
||||
29d1c60c5b64ccdb0bc4e5495135e68e08a872e0ae91f45d9ec34fc135a17981 node-v22.15.0-linux-x64.tar.gz
|
||||
dafe2e8f82cb97de1bd10db9e2ec4c07bbf53389b0799b1e095a918951e78fd4 node-v22.15.0-linux-x64.tar.xz
|
||||
0bc096a279cd7cbc57bf3a6c6570f3ca03b3ab684d1878a1425919e9b6b76317 node-v22.15.0.pkg
|
||||
8e64edbc70a7188873e0f4f07c812fa747c27c477d558b23dcb135bdf5e32298 node-v22.15.0.tar.gz
|
||||
e7c4226d1d92f33ad854d6da4f7e519e77690b8e73f93496881f8c539174d9df node-v22.15.0.tar.xz
|
||||
d0615f16a280f4660a6c83d472a0b2e72122e47f4c82754a5ed950d54d0cbe3d node-v22.15.0-win-arm64.7z
|
||||
737cd7046d96575c2d6cd36e0355afba54b79296b0f403ef4b3b1b5852b10ab6 node-v22.15.0-win-arm64.zip
|
||||
45471a4f77cdaacce971ab7a6698659ed872606f7b4b0aecdfe5689627134cb2 node-v22.15.0-win-x64.7z
|
||||
06067d4f0d463f90ed803d5eca5b039a05dec5d70fc7b7cc254803a59bd0e27c node-v22.15.0-win-x64.zip
|
||||
f9534de57244052cb245ca3af29ac40e947b5735b05b37a28efac7bc82809a8a node-v22.15.0-win-x86.7z
|
||||
961e362568b91340b0229d27a64e3925ce5d807898a86074d61cab578f151843 node-v22.15.0-win-x86.zip
|
||||
499d4a47b9db6db47114a31ceab69c69a7265900a29a5da53746c00d13dbad88 node-v22.15.0-x64.msi
|
||||
1d884cdc5517126c2da00e23b445050a2f1e8b7bcaa57e0feb2c2481d347fb0a node-v22.15.0-x86.msi
|
||||
ebdab2adc9ff5fb09f852b665875af35138b9ee5725a3927bb85d7cfd36e059f win-arm64/node.exe
|
||||
6c43922c747e0c07fcc4ceb64f08e98854a5c1b31665764076f5783556973d16 win-arm64/node.lib
|
||||
fb437ae33e9e5fff6792ba7a1e81d1a1dab5e346b57328e9a0d2ec7b9bdb309d win-arm64/node_pdb.7z
|
||||
7785c2aec7a86efce7707a1abc7dee58bd11e610f748f74e243789f9daff1e13 win-arm64/node_pdb.zip
|
||||
77bdff912b1c569b3e693fe126f619337c3e9d73dafbc4d0bf1d4f1f6a145761 win-x64/node.exe
|
||||
e731ad9af05eabe6bd941442b19dd6037f559a19a2fa8df2a8297ccb46ed7577 win-x64/node.lib
|
||||
8c886b364ac8c65bef429d9d127f457e5c5755d19c4df294e9d187763dc6c3ea win-x64/node_pdb.7z
|
||||
132d18a1f522d20bc63d66b0eabe8089a2643ac61e1585d0269013f01ef1baef win-x64/node_pdb.zip
|
||||
0c20613ca23c8b8e7ca3d1ee5da9173dd9289f9833cff18649c192e4ce42c9b4 win-x86/node.exe
|
||||
5f66397542e27adb288e1742720cedbeaba8a2034ebd58397306b2905f4acd2d win-x86/node.lib
|
||||
93d5377d08c9c5edf71f0c52e801c1ae55ace3e45e107fdac61ec7e04e0e5ff5 win-x86/node_pdb.7z
|
||||
7601c2ca6c1a64b834af03ac2cb78b8485dc501e8b5b980ac4702ff8d6ad0cfb win-x86/node_pdb.zip
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"binary_sha256": "4df429454b0289c6df76d7fd95d41394ebc286191809349b2c1e655b6be494e4",
|
||||
"explicit_make_GOFLAGS": true,
|
||||
"file_and_directory_times_normalized": true,
|
||||
"owner_approval": false,
|
||||
"predecessor_sha256": "4df429454b0289c6df76d7fd95d41394ebc286191809349b2c1e655b6be494e4",
|
||||
"refinement_same_trial": true,
|
||||
"source_locks_preserved": true
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"argv": [
|
||||
"make",
|
||||
"GOFLAGS=-trimpath -buildvcs=false -p=4",
|
||||
"RELEASE_VERSION=11.0.3+nk48.f2788b3f",
|
||||
"FORGEJO_VERSION=11.0.3+nk48.f2788b3f",
|
||||
"FORGEJO_VERSION_API=11.0.3+nk48.f2788b3f",
|
||||
"TAGS=bindata timetzdata sqlite sqlite_unlock_notify",
|
||||
"go-check",
|
||||
"generate-backend",
|
||||
"static-executable"
|
||||
],
|
||||
"phase": "normalized-embedded-and-static-backend",
|
||||
"returncode": 0,
|
||||
"seconds": 7.4
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"argv": [
|
||||
"npm",
|
||||
"ci",
|
||||
"--offline",
|
||||
"--ignore-scripts",
|
||||
"--no-audit",
|
||||
"--no-fund"
|
||||
],
|
||||
"phase": "npm-offline-ci",
|
||||
"returncode": 0,
|
||||
"seconds": 72.94
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
{
|
||||
"buildx.build.provenance": {
|
||||
"builder": {
|
||||
"id": ""
|
||||
},
|
||||
"buildType": "https://mobyproject.org/buildkit@v1",
|
||||
"materials": [
|
||||
{
|
||||
"uri": "pkg:docker/code.forgejo.org/forgejo/forgejo?digest=sha256:41649dff5311664097d29b4d80620403f0b27e7c0eee0b8af79b132a37f3807b\u0026platform=linux%2Famd64",
|
||||
"digest": {
|
||||
"sha256": "41649dff5311664097d29b4d80620403f0b27e7c0eee0b8af79b132a37f3807b"
|
||||
}
|
||||
}
|
||||
],
|
||||
"invocation": {
|
||||
"configSource": {
|
||||
"entryPoint": "Dockerfile"
|
||||
},
|
||||
"parameters": {
|
||||
"frontend": "dockerfile.v0",
|
||||
"args": {
|
||||
"build-arg:SOURCE_DATE_EPOCH": "1752120742",
|
||||
"force-network-mode": "none",
|
||||
"no-cache": ""
|
||||
},
|
||||
"locals": [
|
||||
{
|
||||
"name": "context"
|
||||
},
|
||||
{
|
||||
"name": "dockerfile"
|
||||
}
|
||||
]
|
||||
},
|
||||
"environment": {
|
||||
"platform": "linux/amd64"
|
||||
}
|
||||
}
|
||||
},
|
||||
"buildx.build.ref": "default/default/jm2m1kzz79ebeomnmao91kvkq",
|
||||
"containerimage.config.digest": "sha256:e9e81da43c5ea6895a95b6304337a40874e3275833be5f21fc6ffc104f79a2af",
|
||||
"containerimage.descriptor": {
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"digest": "sha256:9d12779f0936b9ccdbde402a9869eaad8bb91ba724d579b6c2983e8e58dabf96",
|
||||
"size": 2841,
|
||||
"annotations": {
|
||||
"org.opencontainers.image.created": "2025-07-10T04:12:22Z"
|
||||
},
|
||||
"platform": {
|
||||
"architecture": "amd64",
|
||||
"os": "linux"
|
||||
}
|
||||
},
|
||||
"containerimage.digest": "sha256:9d12779f0936b9ccdbde402a9869eaad8bb91ba724d579b6c2983e8e58dabf96"
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import subprocess,pathlib,json,time,os
|
||||
r=pathlib.Path('/tmp/apps-forgejo-release-build-r1');review=pathlib.Path('/tmp/forgejo-independent-r1');env=json.loads((r/'build-env.json').read_text());src=r/'source'
|
||||
# Normalise immutable source metadata before webpack and bindata generation.
|
||||
for root,dirs,files in os.walk(src):
|
||||
dirs[:]=[d for d in dirs if d not in ('node_modules','.git')]
|
||||
for name in files:
|
||||
p=pathlib.Path(root)/name
|
||||
if not p.is_symlink():os.utime(p,(1752105237,1752105237))
|
||||
base=['bwrap','--unshare-net','--ro-bind','/usr','/usr','--ro-bind','/lib','/lib','--ro-bind','/lib64','/lib64','--symlink','usr/bin','/bin','--proc','/proc','--dev','/dev','--tmpfs','/tmp','--bind',str(r),str(r),'--ro-bind',str(review/'go'),str(review/'go'),'--ro-bind',str(review/'gomodcache'),str(review/'gomodcache'),'--chdir',str(src),'--','env','-i']+[k+'='+v for k,v in env.items()]
|
||||
results=[]
|
||||
commands=[('verify-go-modules',['go','mod','verify']),('frontend',['make','frontend']),('embedded-and-static-backend',['make','RELEASE_VERSION=11.0.3+nk48.f2788b3f','FORGEJO_VERSION=11.0.3+nk48.f2788b3f','FORGEJO_VERSION_API=11.0.3+nk48.f2788b3f','TAGS=bindata timetzdata sqlite sqlite_unlock_notify','go-check','generate-backend','static-executable'])]
|
||||
for name,args in commands:
|
||||
if name=='embedded-and-static-backend':
|
||||
for folder in ['public','options','templates','modules/migration/schemas']:
|
||||
for root,dirs,files in os.walk(src/folder):
|
||||
for n in files:
|
||||
p=pathlib.Path(root)/n
|
||||
if not p.is_symlink():os.utime(p,(1752105237,1752105237))
|
||||
start=time.time()
|
||||
with (r/(name+'.log')).open('w') as out:result=subprocess.run(base+args,stdout=out,stderr=subprocess.STDOUT)
|
||||
item={'phase':name,'argv':args,'returncode':result.returncode,'seconds':round(time.time()-start,2)};results.append(item);(r/'source-build-results.json').write_text(json.dumps(results,indent=2));print(json.dumps(item),flush=True)
|
||||
if result.returncode:print((r/(name+'.log')).read_text()[-5000:],flush=True);raise SystemExit(result.returncode)
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
[
|
||||
{
|
||||
"phase": "verify-go-modules",
|
||||
"argv": [
|
||||
"go",
|
||||
"mod",
|
||||
"verify"
|
||||
],
|
||||
"returncode": 0,
|
||||
"seconds": 6.88
|
||||
},
|
||||
{
|
||||
"phase": "frontend",
|
||||
"argv": [
|
||||
"make",
|
||||
"frontend"
|
||||
],
|
||||
"returncode": 0,
|
||||
"seconds": 103.46
|
||||
},
|
||||
{
|
||||
"phase": "embedded-and-static-backend",
|
||||
"argv": [
|
||||
"make",
|
||||
"RELEASE_VERSION=11.0.3+nk48.f2788b3f",
|
||||
"FORGEJO_VERSION=11.0.3+nk48.f2788b3f",
|
||||
"FORGEJO_VERSION_API=11.0.3+nk48.f2788b3f",
|
||||
"TAGS=bindata timetzdata sqlite sqlite_unlock_notify",
|
||||
"go-check",
|
||||
"generate-backend",
|
||||
"static-executable"
|
||||
],
|
||||
"returncode": 0,
|
||||
"seconds": 350.05
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"binary_sha256": "bc34e12a8794e556764ab2325c31615f6ddbcc7a314aabf106b580e470189911",
|
||||
"bytes": 111162624,
|
||||
"path": "/tmp/apps-forgejo-release-build-r1/forgejo-predecessor-no-trimpath",
|
||||
"source_build_complete": true,
|
||||
"reproducibility_accepted": false,
|
||||
"reason": "upstream Make overrides environment GOFLAGS; input directories were not yet normalised"
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"lock_sha256": "29f329b204a2a5f72084b50d59e8445486ff4799450097c6f3908c74e87a233a",
|
||||
"module_cache": "/tmp/forgejo-independent-r1/gomodcache",
|
||||
"public_archives_verified": true
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"node_archive_reused_existing_download": true,
|
||||
"official_checksum_url": "https://nodejs.org/dist/v22.15.0/SHASUMS256.txt",
|
||||
"archive_sha256": "dafe2e8f82cb97de1bd10db9e2ec4c07bbf53389b0799b1e095a918951e78fd4",
|
||||
"official_checksum_refetched_empty_environment": true,
|
||||
"proxy_environment_disabled": true,
|
||||
"HOME_isolated": true,
|
||||
"private_credential_or_config_access": false
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"returncode": 0, "seconds": 46.13}
|
||||
|
|
@ -0,0 +1,198 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Build-only public Forgejo owner recipe; no deployment or publication code."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tarfile
|
||||
import time
|
||||
|
||||
OWNER=Path(__file__).resolve().parents[1]
|
||||
INPUTS=OWNER/'build/forgejo'
|
||||
LOCK=json.loads((INPUTS/'inputs.lock.json').read_text())
|
||||
|
||||
def digest(path):
|
||||
h=hashlib.sha256()
|
||||
with Path(path).open('rb') as f:
|
||||
for block in iter(lambda:f.read(1024*1024),b''):h.update(block)
|
||||
return h.hexdigest()
|
||||
|
||||
def save(path,data):
|
||||
Path(path).write_text(json.dumps(data,indent=2,sort_keys=True)+'\n')
|
||||
|
||||
def check(path,expected):
|
||||
if digest(path)!=expected:raise ValueError('frozen public input digest differs: '+str(path))
|
||||
|
||||
def base_env(work):
|
||||
return {'PATH':str(work/'node-v22.15.0-linux-x64/bin')+':'+str(work/'go/bin')+':/usr/bin:/bin',
|
||||
'HOME':str(work/'home'),'GOTOOLCHAIN':'local','GOPROXY':'off','GOSUMDB':'off','GOVCS':'*:off',
|
||||
'GOPATH':str(work/'gopath'),'GOMODCACHE':json.loads((work/'prepared.json').read_text())['module_cache'],
|
||||
'GOCACHE':str(work/'gocache'),'CGO_ENABLED':'1','GOMAXPROCS':'4',
|
||||
'GOFLAGS':'-trimpath -buildvcs=false -p=4','SOURCE_DATE_EPOCH':str(LOCK['source_date_epoch']),
|
||||
'TZ':'UTC','LC_ALL':'C','NODE_OPTIONS':'--max-old-space-size=4096',
|
||||
'GIT_CONFIG_NOSYSTEM':'1','GIT_CONFIG_GLOBAL':'/dev/null','GIT_TERMINAL_PROMPT':'0',
|
||||
'NPM_CONFIG_USERCONFIG':str(work/'empty.npmrc'),'NPM_CONFIG_GLOBALCONFIG':str(work/'empty-global.npmrc'),
|
||||
'NPM_CONFIG_CACHE':str(work/'npm-cache'),'NPM_CONFIG_REGISTRY':'https://registry.npmjs.org',
|
||||
'NPM_CONFIG_OFFLINE':'true','NPM_CONFIG_AUDIT':'false','NPM_CONFIG_FUND':'false',
|
||||
'NPM_CONFIG_UPDATE_NOTIFIER':'false','PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD':'1'}
|
||||
|
||||
def sandbox(work,env):
|
||||
cache=Path(env['GOMODCACHE'])
|
||||
return ['bwrap','--unshare-net','--ro-bind','/usr','/usr','--ro-bind','/lib','/lib',
|
||||
'--ro-bind','/lib64','/lib64','--symlink','usr/bin','/bin','--proc','/proc','--dev','/dev',
|
||||
'--tmpfs','/tmp','--bind',str(work),str(work),'--ro-bind',str(cache),str(cache),
|
||||
'--chdir',str(work/'source'),'--','env','-i']+[k+'='+v for k,v in env.items()]
|
||||
|
||||
def run(work,name,args,env=None,offline=False,cwd=None):
|
||||
actual=sandbox(work,env)+args if offline else args
|
||||
start=time.time()
|
||||
with (work/(name+'.log')).open('w') as out:
|
||||
proc=subprocess.Popen(actual,cwd=cwd or work,env={"PATH":"/usr/bin:/bin","HOME":str(work/"home")} if offline else env,stdout=out,stderr=subprocess.STDOUT)
|
||||
save(work/'process.json',{'phase':name,'pid':proc.pid,'driver_pid':os.getpid(),'argv':args,'state':'running'})
|
||||
code=proc.wait()
|
||||
result={'phase':name,'argv':args,'returncode':code,'seconds':round(time.time()-start,2)}
|
||||
save(work/(name+'-result.json'),result);save(work/'process.json',dict(result,state='complete',pid=None,driver_pid=None))
|
||||
print(json.dumps(result),flush=True)
|
||||
if code:raise RuntimeError('phase failed; retained log: '+str(work/(name+'.log')))
|
||||
return result
|
||||
|
||||
def checked_inputs(work):
|
||||
for n,h in LOCK['host_tools'].items():check(n,h)
|
||||
for n,h in LOCK['lockfiles'].items():check(work/'source'/n,h)
|
||||
for x in LOCK['reviewed_outputs']:check(work/'source'/x['path'],x['sha256'])
|
||||
|
||||
def prepare(a,work):
|
||||
if work.exists():raise ValueError('workspace must be new; existing trial is never restarted or overwritten')
|
||||
for n,key in [('source_archive','source_archive_sha256'),('go_archive','go_archive_sha256'),('node_archive','node_archive_sha256')]:
|
||||
if not getattr(a,n):raise ValueError('explicit '+n+' required')
|
||||
check(getattr(a,n),LOCK[key])
|
||||
if not a.module_cache:raise ValueError('explicit public module cache required')
|
||||
cache=Path(a.module_cache).resolve();assert cache.is_dir()
|
||||
check(INPUTS/'required-claim-values.patch',LOCK['patch_sha256'])
|
||||
for n,h in LOCK['host_tools'].items():check(n,h)
|
||||
work.mkdir(parents=True);(work/'home').mkdir()
|
||||
with tarfile.open(a.source_archive) as t:
|
||||
# The official archive is digest-bound before any member is extracted.
|
||||
parent=work/'unpacked';parent.mkdir();t.extractall(parent,filter='data')
|
||||
folders=list(parent.iterdir());assert len(folders)==1 and folders[0].is_dir();folders[0].rename(work/'source');parent.rmdir()
|
||||
for n in ['go_archive','node_archive']:
|
||||
with tarfile.open(getattr(a,n)) as t:t.extractall(work,filter='data')
|
||||
for n in ['empty.npmrc','empty-global.npmrc']:(work/n).write_text('')
|
||||
if a.go_cache_seed:shutil.copytree(Path(a.go_cache_seed).resolve(),work/'gocache')
|
||||
if a.npm_cache_seed:shutil.copytree(Path(a.npm_cache_seed).resolve(),work/'npm-cache')
|
||||
subprocess.run(['/usr/bin/git','apply','--check',str(INPUTS/'required-claim-values.patch')],cwd=work/'source',check=True,env={'PATH':'/usr/bin:/bin','HOME':str(work/'home'),'GIT_CONFIG_NOSYSTEM':'1','GIT_CONFIG_GLOBAL':'/dev/null'})
|
||||
subprocess.run(['/usr/bin/git','apply',str(INPUTS/'required-claim-values.patch')],cwd=work/'source',check=True,env={'PATH':'/usr/bin:/bin','HOME':str(work/'home'),'GIT_CONFIG_NOSYSTEM':'1','GIT_CONFIG_GLOBAL':'/dev/null'})
|
||||
save(work/'prepared.json',{'module_cache':str(cache),'lock_sha256':digest(INPUTS/'inputs.lock.json'),'public_archives_verified':True})
|
||||
checked_inputs(work)
|
||||
packages=json.loads((work/'source/package-lock.json').read_text())['packages']
|
||||
for name,package in packages.items():
|
||||
if not name:continue
|
||||
if not package.get('resolved','').startswith('https://registry.npmjs.org/') or not package.get('integrity','').startswith('sha512-'):
|
||||
raise ValueError('non-public/non-integrity-bound npm package: '+name)
|
||||
print(json.dumps({'prepared':True,'lockfile_npm_packages':len(packages)-1}))
|
||||
|
||||
def normalize(folder):
|
||||
epoch=LOCK['source_date_epoch']
|
||||
for root,dirs,files in os.walk(folder):
|
||||
dirs[:]=[x for x in dirs if x not in ('node_modules','.git')]
|
||||
for n in files:
|
||||
p=Path(root)/n
|
||||
if not p.is_symlink():os.utime(p,(epoch,epoch))
|
||||
|
||||
def build(work):
|
||||
checked_inputs(work);env=base_env(work)
|
||||
run(work,'verify-go-modules',['go','mod','verify'],env,True)
|
||||
run(work,'npm-offline-ci',['npm','ci','--offline','--ignore-scripts','--no-audit','--no-fund'],env,True)
|
||||
normalize(work/'source')
|
||||
version=['GOFLAGS=-trimpath -buildvcs=false -p=4','RELEASE_VERSION='+LOCK['release_version'],'FORGEJO_VERSION='+LOCK['release_version'],'FORGEJO_VERSION_API='+LOCK['release_version']]
|
||||
run(work,'frontend',['make']+version+['frontend'],env,True)
|
||||
for n in ['public','options','templates','modules/migration/schemas']:normalize(work/'source'/n)
|
||||
run(work,'embedded-and-static-backend',['make']+version+['TAGS='+LOCK['tags'],'go-check','generate-backend','static-executable'],env,True)
|
||||
checked_inputs(work)
|
||||
save(work/'build.json',{'binary_sha256':digest(work/'source/gitea'),'source_locks_preserved':True,'frontend_built':True,'tags':LOCK['tags'],'image_deployment_selected':False})
|
||||
|
||||
def verify(work):
|
||||
env=base_env(work);binary=str(work/'source/gitea')
|
||||
env.update(GITEA_WORK_DIR=str(work/'verify-work'),GITEA_CUSTOM=str(work/'verify-custom'),GITEA_APP_INI=str(work/'absent-app.ini'))
|
||||
(work/'verify-work').mkdir(exist_ok=True)
|
||||
run(work,'binary-version',[binary,'--version'],env,True)
|
||||
run(work,'new-cli-help',[binary,'admin','auth','add-oauth','--help'],env,True)
|
||||
assert '--required-claim-values' in (work/'new-cli-help.log').read_text()
|
||||
target=work/'embedded-extracted'
|
||||
if target.exists():raise ValueError('retain prior verification; extraction target already exists')
|
||||
run(work,'embedded-extract',[binary,'embedded','extract','--include-vendored','--destination',str(target)],env,True)
|
||||
records={}
|
||||
for folder in ['public','options','templates']:
|
||||
for f in sorted((work/'source'/folder).rglob('*')):
|
||||
if not f.is_file():continue
|
||||
n=str(f.relative_to(work/'source'));q=target/n
|
||||
if not q.is_file() or digest(f)!=digest(q):raise ValueError('embedded asset missing/different: '+n)
|
||||
records[n]=digest(f)
|
||||
assert 'public/assets/js/index.js' in records and 'public/assets/css/index.css' in records
|
||||
assert 'public/assets/licenses.txt' in records
|
||||
save(work/'embedded-files.json',records)
|
||||
readelf=subprocess.run(['/usr/bin/readelf','-l',binary],capture_output=True,text=True,check=True,env={'PATH':'/usr/bin:/bin'})
|
||||
assert 'INTERP' not in readelf.stdout
|
||||
save(work/'verification.json',{'binary_sha256':digest(binary),'embedded_files':len(records),
|
||||
'embedded_file_manifest_sha256':digest(work/'embedded-files.json'),'all_public_options_templates_exact':True,
|
||||
'no_dynamic_interpreter':True,'new_cli_flag_present':True,'network':'bwrap --unshare-net',
|
||||
'source_locks_preserved':True,'architecture_approval':False,'native_admission':False})
|
||||
print((work/'verification.json').read_text())
|
||||
|
||||
def container(work):
|
||||
verified=json.loads((work/'verification.json').read_text());check(work/'source/gitea',verified['binary_sha256'])
|
||||
context=work/'container-context'
|
||||
if context.exists():raise ValueError('container trial already exists; retain evidence')
|
||||
context.mkdir();shutil.copyfile(work/'source/gitea',context/'gitea')
|
||||
os.utime(context/'gitea',(LOCK['source_date_epoch'],)*2)
|
||||
(context/'Dockerfile').write_text('FROM '+LOCK['runtime_base']+'\nARG SOURCE_DATE_EPOCH\nCOPY --chown=0:0 --chmod=755 gitea /app/gitea/gitea\n')
|
||||
os.utime(context/'Dockerfile',(LOCK['source_date_epoch'],)*2)
|
||||
config=work/'docker-config';config.mkdir(exist_ok=True)
|
||||
env={'PATH':'/usr/bin:/bin','HOME':str(work/'home'),'DOCKER_CONFIG':str(config),'DOCKER_HOST':'unix:///var/run/docker.sock'}
|
||||
run(work,'container-build',['docker','buildx','build','--load','--platform',LOCK['platform'],'--pull=false',
|
||||
'--network=none','--provenance=false','--sbom=false','--iidfile',str(work/'image-id'),
|
||||
'--metadata-file',str(work/'image-build-metadata.json'),'--build-arg','SOURCE_DATE_EPOCH='+str(LOCK['source_date_epoch']),str(context)],env)
|
||||
image=(work/'image-id').read_text().strip();assert image.startswith('sha256:')
|
||||
run(work,'container-save',['docker','image','save','--output',str(work/'image.tar'),image],env)
|
||||
with tarfile.open(work/'image.tar') as archive:
|
||||
manifest=json.load(archive.extractfile('manifest.json'));assert len(manifest)==1
|
||||
item=manifest[0];config_bytes=archive.extractfile(item['Config']).read()
|
||||
assert 'sha256:'+hashlib.sha256(config_bytes).hexdigest()==image
|
||||
config=json.loads(config_bytes)
|
||||
base_bytes=(INPUTS/'runtime-base-config.json').read_bytes();assert hashlib.sha256(base_bytes).hexdigest()==LOCK['runtime_base_config_sha256']
|
||||
original=json.loads(base_bytes)
|
||||
assert config['rootfs']['diff_ids'][:-1]==original['rootfs']['diff_ids']
|
||||
for key in ['User','Entrypoint','Cmd','Env','WorkingDir','Volumes']:
|
||||
assert config['config'].get(key)==original['config'].get(key),key
|
||||
assert config['config']['User']=='1000:1000'
|
||||
found=False
|
||||
with tarfile.open(fileobj=archive.extractfile(item['Layers'][-1]),mode='r|*') as layer:
|
||||
for member in layer:
|
||||
if member.name.lstrip('./')=='app/gitea/gitea':
|
||||
h=hashlib.sha256();stream=layer.extractfile(member)
|
||||
for block in iter(lambda:stream.read(1024*1024),b''):h.update(block)
|
||||
assert h.hexdigest()==verified['binary_sha256'];found=True
|
||||
assert found
|
||||
save(work/'container-result.json',{'local_image_id':image,'archive_sha256':digest(work/'image.tar'),
|
||||
'all_base_layers_and_rootless_runtime_config_preserved':True,'final_layer_binary_exact':True,
|
||||
'registry_publication':False,'deployment_tag':None,'host_or_secret_mounts':False,'privileged_containers':False})
|
||||
print((work/'container-result.json').read_text())
|
||||
|
||||
def main():
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('phase',choices=['prepare','acquire-npm','build','verify','container'])
|
||||
parser.add_argument('--workspace',required=True)
|
||||
for n in ['source-archive','go-archive','node-archive','module-cache','go-cache-seed','npm-cache-seed']:parser.add_argument('--'+n)
|
||||
a=parser.parse_args();work=Path(a.workspace).resolve()
|
||||
if not str(work).startswith('/tmp/'):raise ValueError('isolated build workspace must be under /tmp')
|
||||
if a.phase=='prepare':return prepare(a,work)
|
||||
assert json.loads((work/'prepared.json').read_text())['lock_sha256']==digest(INPUTS/'inputs.lock.json')
|
||||
if a.phase=='acquire-npm':
|
||||
checked_inputs(work);env=base_env(work);env['NPM_CONFIG_OFFLINE']='false'
|
||||
return run(work,'npm-public-acquire',['npm','ci','--ignore-scripts','--no-audit','--no-fund'],env,cwd=work/'source')
|
||||
{'build':build,'verify':verify,'container':container}[a.phase](work)
|
||||
|
||||
if __name__=='__main__':main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue