<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Aayush's Technomancy]]></title><description><![CDATA[This is the place where I ramble about all the testing I do.]]></description><link>https://blog.aayushpokharel.com</link><generator>RSS for Node</generator><lastBuildDate>Thu, 21 May 2026 06:52:39 GMT</lastBuildDate><atom:link href="https://blog.aayushpokharel.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Building and Automating a Python Package with Trusted Publishing]]></title><description><![CDATA[There’s a big difference between:
“I wrote a Python script.” and “I built, packaged, versioned, and automated a Python library for PyPI using Trusted Publishing.”
This project started small. I just wa]]></description><link>https://blog.aayushpokharel.com/building-and-automating-a-python-package-with-trusted-publishing</link><guid isPermaLink="true">https://blog.aayushpokharel.com/building-and-automating-a-python-package-with-trusted-publishing</guid><category><![CDATA[education]]></category><category><![CDATA[automation]]></category><category><![CDATA[GitHub Actions]]></category><category><![CDATA[Python]]></category><category><![CDATA[pypi]]></category><dc:creator><![CDATA[Aayush Pokharel]]></dc:creator><pubDate>Tue, 03 Mar 2026 08:30:38 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/66817765c6c9e69428c980f6/15f6b565-7a00-4148-aa33-5d9600e3952b.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>There’s a big difference between:</p>
<p>“I wrote a Python script.” and “I built, packaged, versioned, and automated a Python library for PyPI using Trusted Publishing.”</p>
<p>This project started small. I just wanted to understand how Python packages <em>actually</em> work.</p>
<p>Not just:</p>
<pre><code class="language-bash">pip install -e .
</code></pre>
<p>But:</p>
<ul>
<li><p>What makes a package installable?</p>
</li>
<li><p>What actually happens when you run <code>pip install</code>?</p>
</li>
<li><p>How does PyPI verify uploads?</p>
</li>
<li><p>How do you automate releases properly?</p>
</li>
</ul>
<hr />
<h3>The Idea Behind the Project</h3>
<p>In many projects, paths get messy.</p>
<ul>
<li><p>Hardcoded directories creep in.</p>
</li>
<li><p>Relative paths become brittle.</p>
</li>
<li><p>Boilerplate repeats across repositories.</p>
</li>
</ul>
<p>I was building multiple data scraping projects and found myself rewriting the same directory resolution logic again and again.</p>
<p>So I built a small utility package that dynamically resolves:</p>
<ul>
<li><p>Project root</p>
</li>
<li><p>Absolute project root</p>
</li>
<li><p>Resource locations (like WebDriver paths)</p>
</li>
</ul>
<p>Simple problem. Clean solution.</p>
<p>I just wanted a reusable library, so I didn’t have to repeat boilerplate code in every scraping project.</p>
<hr />
<h3>The Real Goal: Make It Real</h3>
<p>I didn’t want this to be just a GitHub repository.</p>
<p>I wanted it to be installable via:</p>
<pre><code class="language-bash">pip install directionfinder
</code></pre>
<p>That’s when things got interesting.</p>
<p>This article walks through that journey — step by step.</p>
<hr />
<h3>Step 1 — Modern Packaging with <code>pyproject.toml</code></h3>
<p>Instead of using the older <code>setup.py</code>, I followed <strong>PEP 621</strong> and used <code>pyproject.toml</code>. I have both files in my git repo though.</p>
<pre><code class="language-toml">[build-system]
requires = ["setuptools&gt;=61.0"]
build-backend = "setuptools.build_meta"

[project]
name = "directionfinder"
version = "0.6.1"
description = "Utility package to dynamically resolve project directory paths."
readme = "README.md"
requires-python = "&gt;=3.7"

authors = [
  { name="Aayush Pokharel", email="official@aayushpokharel.com" }
]

license = { text = "MIT" }

classifiers = [
  "Programming Language :: Python :: 3",
  "Operating System :: OS Independent",
]
</code></pre>
<h3>Build Locally</h3>
<pre><code class="language-bash">pip install build
python -m build
</code></pre>
<p>Artifacts generated:</p>
<pre><code class="language-plaintext">dist/
  directionfinder-0.6.1.tar.gz
  directionfinder-0.6.1-py3-none-any.whl
</code></pre>
<p>That felt satisfying.</p>
<p>But uploading manually using <code>twine</code> felt… outdated.</p>
<hr />
<h3>Step 2 — Trusted Publishing (No Tokens!)</h3>
<p>Instead of storing an API token in GitHub Secrets, I used <strong>Trusted Publishing</strong>.</p>
<p>Trusted Publishing uses <strong>OIDC (OpenID Connect)</strong> to let GitHub prove its identity directly to PyPI.</p>
<ul>
<li><p>No passwords.</p>
</li>
<li><p>No API tokens.</p>
</li>
<li><p>No secrets to rotate.</p>
</li>
</ul>
<p>That’s beautiful.</p>
<p>Here’s how the trust chain works:</p>
<p>![<a href="https://cdn.hashnode.com/uploads/covers/66817765c6c9e69428c980f6/f2ae8dcc-9a8a-4619-9c92-38a6c4d9cdcf.png%5D">https://cdn.hashnode.com/uploads/covers/66817765c6c9e69428c980f6/f2ae8dcc-9a8a-4619-9c92-38a6c4d9cdcf.png]</a>(<a href="https://cdn.hashnode.com/uploads/covers/66817765c6c9e69428c980f6/f2ae8dcc-9a8a-4619-9c92-38a6c4d9cdcf.png">https://cdn.hashnode.com/uploads/covers/66817765c6c9e69428c980f6/f2ae8dcc-9a8a-4619-9c92-38a6c4d9cdcf.png</a> align="middle")</p>
<p>GitHub generates a short-lived identity token.<br />PyPI verifies that the repository and workflow match the registered trusted publisher.</p>
<p><strong>If everything matches → upload allowed.</strong></p>
<hr />
<h3>Step 3 — GitHub Actions Workflow</h3>
<p>I created:</p>
<pre><code class="language-plaintext">.github/workflows/publish.yml
</code></pre>
<pre><code class="language-yaml">name: Publish to TestPyPI

on:
  push:
    tags:
      - "v*"

jobs:
  publish:
    runs-on: ubuntu-latest

    permissions:
      id-token: write
      contents: read

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: "3.x"

      - run: |
          python -m pip install --upgrade pip
          pip install build

      - run: python -m build

      - uses: pypa/gh-action-pypi-publish@release/v1
        with:
          repository-url: https://test.pypi.org/legacy/
</code></pre>
<p>Notice this part:</p>
<pre><code class="language-yaml">permissions:
  id-token: write
</code></pre>
<p>This is required for OIDC authentication.</p>
<p>Without it, Trusted Publishing will fail.</p>
<hr />
<h3>Step 4 — Version-Based Release Flow</h3>
<p>Publishing is triggered by tags:</p>
<pre><code class="language-bash">git tag v0.5.0
git push origin v0.5.0
</code></pre>
<p>Behind the scenes:</p>
<p>![](<a href="https://cdn.hashnode.com/uploads/covers/66817765c6c9e69428c980f6/21f71f86-a2cf-42af-a299-1d531b7042be.png">https://cdn.hashnode.com/uploads/covers/66817765c6c9e69428c980f6/21f71f86-a2cf-42af-a299-1d531b7042be.png</a> align="middle")</p>
<p>When it worked for the first time, I just stared at the green checkmark.</p>
<p>It felt earned.</p>
<hr />
<h3>The Bugs Along the Way</h3>
<p>It wasn’t smooth and I did make some mistakes.</p>
<p>Here’s what broke.</p>
<h3>Workflow Didn’t Trigger</h3>
<p>Because tags must be pushed.</p>
<p>Creating a tag locally is not enough:</p>
<pre><code class="language-bash">git push origin v0.5.0
</code></pre>
<p>Otherwise, GitHub Actions never runs.</p>
<h3>HTTP 400 Bad Request</h3>
<p>That one was painful.</p>
<p>It turned out my Trusted Publisher was registered for:</p>
<pre><code class="language-plaintext">direction_pkg
</code></pre>
<p>But my actual project name was:</p>
<pre><code class="language-plaintext">directionfinder
</code></pre>
<p><strong>Mismatch = rejection.</strong></p>
<p>Lesson learned:</p>
<p>Project name must match exactly across:</p>
<ul>
<li><p><code>pyproject.toml</code></p>
</li>
<li><p>PyPI project name</p>
</li>
<li><p>Trusted Publisher configuration</p>
</li>
</ul>
<p>Even a small mismatch causes failure.</p>
<h2>Version Conflicts</h2>
<p>You cannot overwrite a version on PyPI.</p>
<p>If <code>0.6.0</code> already exists, you must bump:</p>
<pre><code class="language-toml">version = "0.6.1"
</code></pre>
<p>And then:</p>
<pre><code class="language-bash">git tag v0.6.1
git push origin v0.6.1
</code></pre>
<p>Version discipline matters.</p>
<hr />
<h3>Automatic Attestations (Unexpected Bonus)</h3>
<p>Another surprise:</p>
<p>GitHub automatically generated digital attestations using <strong>Sigstore</strong>.</p>
<p>This means:</p>
<ul>
<li><p>The package build is cryptographically verifiable</p>
</li>
<li><p>The provenance is recorded in a transparency log</p>
</li>
<li><p>The supply chain is traceable</p>
</li>
</ul>
<p>Modern Python packaging is not just “uploading files.”</p>
<p>It’s about trust.</p>
<p>Here’s the bigger picture:</p>
<p>![](<a href="https://cdn.hashnode.com/uploads/covers/66817765c6c9e69428c980f6/462511fe-4edb-4423-aa4d-0fee7397d979.png">https://cdn.hashnode.com/uploads/covers/66817765c6c9e69428c980f6/462511fe-4edb-4423-aa4d-0fee7397d979.png</a> align="middle")</p>
<p>This is no longer “just a Python script.”</p>
<p>This is CI/CD + supply-chain-aware packaging.</p>
<hr />
<h2>Installing from TestPyPI</h2>
<p>After successful publishing:</p>
<pre><code class="language-bash">pip install \
  --index-url https://test.pypi.org/simple/ \
  --extra-index-url https://pypi.org/simple \
  directionfinder
</code></pre>
<p>And it works. That feeling is different from running a local script.</p>
<blockquote>
<p>The extra flags in the install command tell <code>pip</code> where to look for packages and how to resolve dependencies. The <code>--index-url https://test.pypi.org/simple/</code> flag instructs <code>pip</code> to use <strong>TestPyPI’s Simple API index</strong> as the primary source for locating the package (<code>directionfinder</code> in this case). However, TestPyPI usually does not host common dependencies like <code>setuptools</code>, <code>requests</code>, or other third-party libraries. That’s why we add <code>--extra-index-url https://pypi.org/simple</code>. This flag tells <code>pip</code> to fall back to the real PyPI index if a dependency is not found on TestPyPI. In short, <code>--index-url</code> defines the main package source, and <code>--extra-index-url</code> acts as a secondary lookup location to ensure dependency resolution works smoothly.</p>
</blockquote>
<hr />
<h2>What This Project Taught Me</h2>
<ul>
<li><p>Packaging is more than code.</p>
</li>
<li><p>Versioning is a discipline.</p>
</li>
<li><p>Automation removes human error.</p>
</li>
<li><p>Trusted Publishing is the future.</p>
</li>
<li><p>Secure software supply chain matters.</p>
</li>
<li><p>Debugging CI is part of real engineering.</p>
</li>
</ul>
<p>Most importantly:</p>
<blockquote>
<p>Small projects become powerful when treated professionally.</p>
</blockquote>
<hr />
<h2>Final Thoughts</h2>
<p>This started as:</p>
<blockquote>
<p>“I want to understand how Python packages work.”</p>
</blockquote>
<p>It ended as:</p>
<blockquote>
<p>“I built an automated, secure, versioned, supply-chain-aware release pipeline.”</p>
</blockquote>
<p>And honestly, that shift in mindset is the real win.</p>
<p>If you're learning Python packaging:</p>
<p>Don’t stop at local builds.</p>
<p><strong>Automate it.<br />Break it.<br />Fix it.<br />Secure it.<br />Understand it.</strong></p>
<p>That’s where growth happens. You can find my git repo for this project at:<br /><a href="https://github.com/AayushPokharel/directions_pkg">github.com/AayushPokharel/directions_pkg</a></p>
]]></content:encoded></item><item><title><![CDATA[My Intro To Tech Blogging]]></title><description><![CDATA[I am starting this blog with the hope that I will continue to write about stuff I come across in my journey into computer science.  
This is just a humble placeholder message to place a keystone at the start of my blogging journey.]]></description><link>https://blog.aayushpokharel.com/my-intro-to-tech-blogging</link><guid isPermaLink="true">https://blog.aayushpokharel.com/my-intro-to-tech-blogging</guid><category><![CDATA[Hello World]]></category><dc:creator><![CDATA[Aayush Pokharel]]></dc:creator><pubDate>Sun, 30 Jun 2024 15:51:56 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/z8y36JocqkU/upload/ce10c5dd9f1d1029ace3f989229f4ff8.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I am starting this blog with the hope that I will continue to write about stuff I come across in my journey into computer science.  </p>
<p>This is just a humble placeholder message to place a keystone at the start of my blogging journey.</p>
]]></content:encoded></item></channel></rss>