Semaphore: Recent Episodes

None

The best CI/CD tool for high-performance engineering teams.

View Details

While AI coding assistants have become mainstream, applying generative AI to infrastructure and operations is still emerging. The promise is huge—but so are the challenges, from hallucinated insights to navigating noisy telemetry.

In this episode of Semaphore Uncut, Gou Rao—AI infrastructure veteran and co-founder of Neubird—joins Darko to explore how LLMs can support DevOps teams not just with suggestions, but with reasoning, context gathering, and real-time incident diagnosis. He shares the vision behind Hawkeye, Neubird’s “digital engineer,” and what it means to build agentic systems that think and operate like humans.

A Career Rooted in Data and AIGou’s background in AI and enterprise systems goes back to his graduate work at the University of Pennsylvania. Over the years, he’s applied data science to solve tough infrastructure problems, most recently as part of the founding team at Portworx, a cloud-native storage platform.

At Neubird, Gou brings together his two longtime passions: data structures and AI. His team is now focused on building tools that use LLMs not just to generate content, but to extract operational insights from sprawling, high-volume telemetry systems.

From Stack Traces to Reasoning EnginesMost engineers today interact with LLMs in a straightforward way: paste in a stack trace, ask for help, and get a response. But Neubird’s thesis is different.

“The hard part isn’t solving the stack trace,” Gou says. “It’s finding it.”

Neubird’s product, Hawkeye, acts as a reasoning engine across the entire telemetry stack—logs, metrics, traces, configuration, alerts. Rather than using retrieval-augmented generation, it reverses the pattern: LLMs decide what needs to be found, and the system fetches it through native queries (like PromQL or Splunk searches). That separation improves both accuracy and auditability.

Building a Digital EngineerThe team designed Hawkeye to mirror how a human SRE would think: identify symptoms, investigate telemetry sources, reason about possible causes, and suggest remediations. Behind the scenes, different models (Claude, Mistral, Llama3) are orchestrated depending on task complexity.

Each inference takes a few minutes—not seconds—but that’s intentional. Speed isn’t the goal. Correctness is.

To build trust, Hawkeye shows its chain of thought as it works. For example, it might check a Prometheus metric, then search for recent config changes, then correlate that with a specific GitHub commit. While it doesn’t take action on its own yet, it can generate pull requests, annotate incident tickets, or suggest Terraform changes for review.

Telemetry-First, Not Model-FirstA major design principle behind Hawkeye is accuracy. Gou explains that they never rely solely on model output. If a model says, “Check if CPU usage is high,” the system runs an actual Prometheus query to verify that.

That tight integration with telemetry also means Hawkeye adapts to your environment. During onboarding, teams provide read-only access to their observability tools and infrastructure. From there, Hawkeye builds context, including preferences, frequently failing clusters, and past incidents stored in systems like ServiceNow.

While global model training happens based on aggregated feedback, environment-specific memory—like your preferred verbosity or known flaky services—is stored separately in a vector database to shape interactions without retraining the model.

Scaling from Startups to EnterprisesHawkeye is already proving useful across a wide customer base. One small company with a single SRE uses it to cut through noisy alerts and stay focused on critical issues. At the other end of the spectrum, large banks are integrating Hawkeye into high-stakes payment systems to augment their SRE teams and reduce stress and churn.

No matter the size, the goal is the same: reduce time spent sifting through logs and alerts, and make engineers more effective.

What About Autonomous Actions?Right now, Hawkeye only recommends actions—it doesn’t apply changes directly. That might change, but only with time and trust.

“There’s an unpredictability in generative AI that makes people cautious,” Gou says. “We’re not ready to go full self-driving in production. But maybe we’ll start with safe environments—nightly builds, staging systems, and so on.”

Like Tesla’s autopilot, Gou believes the future will include more automation, but only after the systems prove themselves repeatedly in controlled scenarios.

Trying Out HawkeyeInterested teams can try Hawkeye through Neubird’s website or directly via AWS and Azure marketplaces. A playground environment helps teams test key use cases before connecting real production data.

Once deployed, Hawkeye integrates seamlessly with observability platforms like Prometheus, Azure Monitor, or Splunk. Whether used in real-time triage or as an asynchronous assistant to incident response tools like PagerDuty or Jira, Hawkeye aims to be a thinking partner—not just another bot.

Final Thoughts: A Teammate, Not a ToolGou is clear-eyed about the limitations: Hawkeye isn’t magic. It doesn’t solve every incident or replace your ops team. But it augments engineers by handling the tedious groundwork—surfacing relevant signals, correlating issues, and providing well-reasoned suggestions.

“It’s there to take the grunt work off your plate,” Gou says. “So you can focus on the interesting stuff.”

And in a world where SREs are under constant pressure, that’s more than welcome.

Follow Gou Rao and Neubird🌐 Website: neubird.ai
💼 LinkedIn: Gou Rao | Hawkeye

The post Gou Rao on Agentic Systems in DevOps appeared first on Semaphore.

View Details

We’re excited to share what’s new in Semaphore v1.3 — a release focused on empowering contributors, improving the local development experience, and making advanced CI/CD features more accessible to small teams.

Whether you’re looking to make your first contribution or run Semaphore in your organization, this update brings significant improvements across the board.

Contributing Just Got a Lot EasierSince open-sourcing Semaphore, we’ve welcomed contributions, but we knew the developer experience needed refinement. With v1.3, we’ve made that process much smoother:

  • Forked PRs now trigger CI automaticallyNo more waiting for manual approvals from our team to run tests. As soon as you open a pull request, CI/CD kicks in.
  • Public access to pipeline jobsYou can now inspect test results and logs from your fork, making debugging and iteration significantly easier.
  • Improved contributor documentationOur DEVELOPMENT.md and CONTRIBUTING.md files now explain everything from tooling to branch naming conventions and commit message formatting.
  • Local development with ScaffoldYou can now run Semaphore locally using Skaffold. We’ve consolidated our microservices to reduce the resource footprint to around 7GB RAM and 5 CPUs — and we’re actively working on lowering it even more.

First-Time Contributors SpotlightWe’re thrilled to welcome two new external contributors:

  • Michał Matyas fixed a timestamp formatting issue in the activity monitor UI.
  • Fred resolved a YAML switching bug in our visual editor and fixed typos in the getting started guide.

Thank you both! 🙌

You can find more opportunities labeled as good first issue on our GitHub board. These are ideal if you’re looking to get familiar with our codebase.

Better Workflow Navigation & FilteringWe’ve improved how you browse workflow history:

  • Stateful paginationThe URL now retains your place when navigating between pages or sharing links.
  • Date-based filteringYou can filter workflows by specific date ranges — helpful when tracking down long-running or historical builds.

Announcing the Semaphore Enterprise Edition — Free for Small TeamsWe’re introducing a new Enterprise Edition of Semaphore. And it’s free for small teams!

If your company has:

  • Fewer than 50 employees
  • Less than $5M in annual recurring revenue

You can use the full power of Semaphore, including advanced RBAC and compliance features, at no cost. Just install the Enterprise edition using Helm and request a free license key. More details in our Enterprise Edition docs.

🐛 Coming Soon: Bug Bounties via AlgoraWe’re also launching a bug bounty program in partnership with Algora.io. You’ll soon be able to earn rewards for fixing issues and contributing features. Keep an eye on our GitHub and README for the official launch.

🔭 What’s Next in v1.4?Here’s a sneak peek of what’s coming:

  • Partial pipeline rebuilds via the UIAlready available through our API, this long-requested feature is making its way into the UI.
  • Further local dev optimizationsWe’re aiming to bring down CPU and memory requirements even more.
  • More good-first-issuesExpect a growing list of approachable tasks for new contributors.

Thanks to everyone who contributed to this release — both inside and outside the Semaphore team. Version 1.3 is a major step toward making Semaphore easier to contribute to and more accessible to a wider community.

If you haven’t already, check out the repo, give us a ⭐, and let us know what you think.

Happy building!

The post Semaphore v1.3 Release Roundup: A Big Leap for Contributors and Small Teams 🚀 appeared first on Semaphore.

View Details

What really happens after you push code to your Git repository? Your pipeline kicks off, tests run, and builds are triggered. But, beneath the surface, a complex orchestration engine is making all the right moves.

At Semaphore, that engine is called Plumber.

Plumber is responsible for interpreting pipeline definitions, scheduling jobs, and coordinating execution across distributed agents — all while ensuring reliability and scalability. In this post, I’ll show you:

  • How we built it using a finite state machine
  • Why we chose Elixir
  • How this architecture helps Semaphore scale CI/CD for thousands of developers.

Understanding Semaphore’s Pipeline ModelSemaphore pipelines are made up of blocks and jobs:

  • A pipeline defines your CI/CD workflow.
  • Blocks group jobs together and define order—sequential or parallel.
  • Jobs are execution units that run commands.
  • Agents are the machines that actually execute the jobs.

This structure allows teams to model anything from basic test pipelines to complex multi-stage deployments.

From Git Push to Pipeline ExecutionWhen you push code:

  1. Your Git provider sends a webhook to Semaphore.
  2. The Hooks Processor validates and transforms the event.
  3. The Pipeline Processor—this is where Plumber comes in—fetches the YAML definition and starts orchestration.
  4. The Job Processor prepares and assigns jobs to available agents.
  5. Agents execute jobs and report results back to the platform.

Plumber is the service that turns a definition into a real, running pipeline.

Why a State Machine?We designed Plumber around a finite state machine (FSM) model. Each pipeline transitions through a known set of states:

  • initializing: YAML is parsed and blocks/jobs are created.
  • pending: Conditions (like branch filters) are evaluated.
  • running: Jobs are executed in the correct order.
  • done: The result is finalized and published.

Each state is handled by a dedicated Elixir Looper process that executes state-specific logic. This keeps our system modular, predictable, and easy to maintain.

The Tech StackHere’s what powers Plumber:

  • Elixir: Built for concurrency, fault tolerance, and lightweight processes.
  • PostgreSQL: Provides transactional integrity for all state transitions.
  • RabbitMQ: Ensures reliable communication between services.
  • Kubernetes: Handles deployment, scaling, and recovery.

This stack lets us scale horizontally, recover from failure, and roll out changes with confidence.

Handling Failures GracefullyThings can go wrong. A worker might crash mid-job, or an external dependency might fail. To prevent stuck pipelines, we added a stuck state and a recovery loop.

A dedicated recovery worker detects pipelines that have stalled, retries them with exponential backoff, and uses a circuit breaker to stop retrying if the issue persists. This allows the system to self-heal while keeping other pipelines unaffected.

Extending Pipeline BehaviorThanks to the FSM model, adding new features is straightforward.

Stopping Pipelines ManuallyWhen a user stops a pipeline mid-run, we transition it into a stopping state. All running jobs are halted before the pipeline is done.

Queuing PipelinesIf multiple commits are pushed to the same branch in a short period, they shouldn’t run in parallel. We added a queuing state so that newer pipelines wait for earlier ones to finish.

Recap: Why It WorksPlumber’s architecture supports the demands of modern CI/CD by focusing on a few key principles:

  • Reliability: Supervision trees and Kubernetes health checks keep workers available.
  • Scalability: Looper workers scale horizontally across pods.
  • Fault Tolerance: Stuck state recovery prevents cascading failures.
  • Extensibility: New pipeline logic fits naturally into the state machine.

It’s a system designed to grow with developers’ needs, and it’s been proven in production every day.

Learn MoreSemaphore is open source. You can check out the code and dive deeper into how it works:

🔗 Explore it on GitHub
💬 Join our Discord to ask questions (will add link)
📺 Watch related talks on our YouTube channel

Happy building!

The post The Secrets Behind Plumber: Managing Thousands of Pipelines at Semaphore appeared first on Semaphore.

View Details

Since we open sourced Semaphore with the Community Edition, I wanted to make a detailed comparison with Jenkins, the most popular and well-known open-source CI platform – by far. In this post, I’ll break down the differences in licensing, hardware requirements, installation, access control, redundancy, features, scalability, performance, and maintenance.

By the end, you’ll have a clear understanding of the strengths and weaknesses of each platform, helping you decide which one aligns best with your project needs.

Comparison CategoriesBefore starting, let’s break down the categories we’re going to use in this comparison.

| Feature | Jenkins | Semaphore CE | | --- | --- | --- | | License | MIT | Apache-2.0 | | Hardware Requirements | 4 GB RAM, 1 CPU | 16 GB RAM, 8 CPUs | | Installation | Docker or standalone service | Kubernetes and Helm | | Access Control | Plugin-based | Built-in RBAC | | Redundancy | Active-Passive Setup | Kubernetes-native | | Monorepo Support | Only via plugins | Natively suported | | Test Reporting | Built-in | Built-in Dashboard | | Notifications | Plugins required | Built-in Webhooks | | Scalability | via Worker Nodes | Kubernetes native scalability and self-hosted agents | | Parallelization | Must be specifically configured | Default | | Maintenance | High (Plugin-Dependent) | Low (Minimal Setup) |

License ComparisonJenkins is licensed under the MIT License, making it highly permissive for modification and redistribution.

Semaphore CE, on the other hand, is licensed under Apache-2.0, a similarly permissive license that also includes protections against patent litigation.

| Aspect | MIT License | Apache-2.0 License | | --- | --- | --- | | Permissions | Permissive, allows modification and redistribution | Permissive, allows modification and redistribution | | Conditions | Requires inclusion of the original license | Requires inclusion of the original license, NOTICE file, and state of modifications | | Limitations | No liability or warranty | No liability or warranty | | Patent Protection | No patent protection | Includes explicit patent protection | | Sublicensing | Allowed | Allowed | | Attribution | Required | Required |

Scalability and PerformanceSemaphore CE was designed as a cloud-native platform, leveraging Kubernetes and self-hosted agents for distributed workloads.

Jenkins scales using additional nodes, but requires more manual setup and maintenance.

Semaphore CE’s autoscaler further simplifies scaling for large workloads on demand when using AWS for compute.

ParallelizationJenkins supports parallelization using the parallel keyword in its Jenkinsfile, but all jobs run in the same environment, potentially causing race conditions.

Semaphore CE runs each job in a separate Docker container (or VM) by default, enabling easier and more reliable parallel execution.

Hardware RequirementsJenkins has relatively modest hardware requirements, recommending at least 4 GB of RAM and 1 CPU, with scaling recommendations based on job count and agent connections (1 CPU per 100 jobs).

Semaphore CE, designed to run on Kubernetes, has higher minimum requirements with at least 16 GB RAM and 8 CPUs for the control plane, making it more resource-intensive but also more powerful in terms of handling larger workloads.

InstallationJenkins can run fully inside a firewall and can be deployed using Docker, a standalone package, and run as a service.

In contrast, Semaphore CE has more requirements:

  • a public IP address
  • a domain
  • a TLS certificate

This makes Semaphore’s setup slightly more involved but also more secure and modern in architecture.

Access ControlJenkins requires plugins for role-based access control, making it more customizable but also more complex to set up. Most Jenkins installations use the matrix authorization strategy plugin for this purpose.

Semaphore CE provides built-in role-based access control, allowing for straightforward user and permission management without the need for additional plugins.

RedundancyRedundancy in Jenkins is achieved using an active-passive strategy, where servers share a mount point, allowing one to take over if the other fails.

Semaphore CE leverages Kubernetes’ built-in redundancy, allowing for more robust failover and scaling capabilities.

FeaturesLet’s break down some features that I think we can’t live without in any software project.

Monorepo SupportJenkins can do monorepo if we configure multi-branch pipelines and use plugins, but setting this can be complex and feels brittle

Semaphore CE natively supports monorepos with the ‘change_in’ conditions, allowing for targeted job execution based on specific file changes, reducing unnecessary runs and optimizing resource usage.

Test ReportingBoth platforms support JUnit-based reports. Jenkins shows a success/failure graph over time and offers per-workflow reports.

In the meantime, Semaphore CE offers a built-in test dashboard with categorization, filtering, and test history tracking, making it more user-friendly and less reliant on plugins.

NotificationsJenkins requires plugins to send notifications to external services like Slack or Discord.

Semaphore CE includes built-in webhook support, enabling easier integration with popular communication tools without additional plugin installations.

MaintenanceJenkins’ plugin ecosystem can become a maintenance burden, requiring regular updates and conflict resolution.

Semaphore CE, by contrast, does not rely on any plugins to work, focusing on out-of-the-box functionality that reduces maintenance overhead.

ConclusionChoosing between Jenkins and Semaphore CE comes down to project size, team expertise, and infrastructure preferences. Jenkins offers greater flexibility and an extensive plugin ecosystem but demands more maintenance.

Semaphore CE provides a streamlined, Kubernetes-native experience, reducing setup time and operational complexity at the cost of higher hardware requirements.

Both platforms are open-source, so the best approach is to try both and evaluate based on specific project needs.

Thank you for reading, and happy building!

The post Semaphore Community Edition vs. Jenkins appeared first on Semaphore.

View Details

When we decided to open source Semaphore, we knew it wouldn’t be just about flipping a switch and making the repo public. A lot had to change, especially how we test and manage contributions.

Our CI/CD pipeline had been fine-tuned over the years to work with our internal infrastructure. But if we wanted to support real-world use cases—developers deploying Semaphore on Google Cloud, AWS, or even their own machines—we needed something new.

We needed to validate that every release of Semaphore would run smoothly anywhere. That meant spinning up fresh infrastructure, installing the app, running end-to-end tests, and tearing it all down.

So we built ephemeral environments. This is how we did it—and how it ended up being a lot harder than I thought.

The Goal: From Prod-Centric to PortableOur infrastructure had grown organically over a decade. Everything—our app config, testing setup, CI logic—was tailored to our exact environment.

Once we went open source, that was no longer acceptable. We had to prove that Semaphore could run cleanly in new environments, without the crutches of our internal setup.

So we set out to change the pipeline.

Here’s what the goal looked like:

  1. Package the app using Helm charts.
  2. Spin up a brand-new Kubernetes cluster.
  3. Install Semaphore using those charts.
  4. Run end-to-end tests.
  5. Tear it all down when we’re done.

Seems reasonable, right?

First Stop: Google CloudSince our production runs on Google Cloud, starting with GKE made sense. We already used Terraform for our infrastructure, so we figured we could just repurpose what we had. But it turns our that wouldn’t work at all.

The main issue with Terraform is that our config had 270+ resources, including a bunch of networking and security stuff we didn’t need for the Community Edition. It was testing overkill.

So we started from scratch. The minimal version included:

  • A VPC (with default subnets)
  • A Kubernetes cluster
  • A node pool
  • A resource for storing TLS certificates
  • A static IP

Just five resources. That was a huge win.

We hooked up a Helm chart to install Semaphore, passed in a domain name, and used a Google Load Balancer to expose it.

Then I hit my first real snag.

The Load Balancer ProblemSemaphore’s installer pings itself via the public domain to verify the install worked. But the load balancer doesn’t exist yet when installation starts. So, the installation fails.

The DNS needs to point to something. The solution was to reserve a static IP during provisioning, point the domain to that IP in DNS, and then make sure the load balancer uses that reserved IP once it spins up.

It worked. Not elegant, but functional.

From Manual to CIOnce we had the GCP-based ephemeral environment working manually,weI turned it into a CI workflow:

  • Generate certs
  • Provision infrastructure with Terraform
  • Install Semaphore with Helm
  • Run end-to-end tests
  • Destroy everything

Each step runs in its own CI job, and we pass data between jobs using artifacts. The flow was finally shaping up.

Time to try the same on AWS.

Then Came EKSWe assumed this would be a “copy/paste and rename” kind of situation. But, yet again, we found a lot of unforeseen problems.

Here’s what broke immediately:

  • Persistent volumes didn’t work. Unlike GCP, EKS doesn’t come with default storage classes.
  • Pods didn’t schedule. Turns out, AWS limits pods per node based on IP allocation via ENIs. We had to split into multiple nodes.
  • Load balancers didn’t auto-provision. GKE handles this for you. EKS doesn’t—unless you install a separate controller.
  • No static IPs or DNS reservations. You only get the DNS name after the LB is created.

So we added:

  • AWS Load Balancer Controller (LB), to actually spin up the LB from our Ingress definitions
  • External DNS, to update our DNS zone automatically when the LB is ready

The extra complexity was painful, but it finally mirrored the GCP setup.

Meanwhile, Someone Just Used a VMWhile we were deep in Kubernetes land, a teammate spun up a minimal environment using a single VM and k3s.

It booted in 45 seconds. No load balancers. No controllers. No YAML debugging. It just ran.

We now use this approach for fast, local testing during feature development. When we need to validate against “real” infrastructure, we run the GCP and AWS ephemeral environments before a release.

What We Learned (the Hard Way)Here’s what we’d tell our past selves before starting the project:

  1. Cloud services aren’t interchangeable. GCP and AWS might both offer “managed Kubernetes,” but the assumptions, defaults, and gotchas are wildly different.
  2. Strip it down. Start with the simplest working Terraform you can. Add features later.
  3. Expect weird infrastructure bugs. Load balancer DNS, pod scheduling limits, and missing storage classes—these took the most time to figure out.
  4. Use automation to your advantage. CI pipelines, Terraform, Helm—once wired together, they save hours of repeat work.

Where We Are NowToday, every version of Semaphore goes through:

  • Lightweight CI validation in a single-VM k3s setup
  • Full end-to-end tests in GCP and AWS-based ephemeral environments

And everything is triggered automatically in CI.

What used to be a manual process tied to our infrastructure is now a portable, repeatable, and testable flow.

It wasn’t the easiest migration, but it was one of the most important things we’ve done for open-sourcing Semaphore.

If you’re working on something similar—or thinking about building ephemeral environments—feel free to drop by our Discord server or check out the repository. We’re always happy to chat.

The post Ephemeral Environments for Testing Open Source Projects appeared first on Semaphore.

View Details

In this episode of Semaphore Uncut, Darko Fabijan welcomes Marcelo Calbucci—engineer, startup founder, and author of The PR FAQ Book. Marcelo shares insights from his time at Amazon, where he encountered a decision-making framework that changed how he thinks about product development: the PR FAQ.

What starts as an internal press release quickly becomes a strategic tool to align teams, evaluate product ideas, and make better decisions—before writing a single line of code.

From Startups to Amazon—and Back AgainMarcelo’s journey spans startups, big tech, and now authorship. Originally from Brazil, he kicked off his career at Microsoft working on Bing, then founded multiple startups across Seattle and London. After 18 years in the startup world, he joined Amazon—a drastic shift in scale and structure.

That’s where he discovered the PR FAQ framework.

“I saw how useful it was, not just inside Amazon,” Marcelo says, “but for any founder, product leader, or engineer making strategic decisions.”

What Is a PR FAQ?PR FAQ stands for Press Release and Frequently Asked Questions. It’s more than a document—Marcelo describes it as a three-part system:

  • A six-page narrative outlining the vision and FAQs
  • A precise writing style that emphasizes clarity and data over buzzwords
  • A method for team collaboration and decision-making

The structure is simple:

  1. Page 1: A fictional press release, imagining the product is already launched
  2. Page 2: Customer FAQs
  3. Pages 3–6: Internal FAQs addressing strategy, feasibility, and impact

Unlike traditional planning documents, a PR FAQ avoids details about tech stacks, feature lists, or timelines. Instead, it focuses on why the project matters, what problem it solves, and who it serves.

Working Backwards: Strategy Before ExecutionAt the heart of the PR FAQ is a technique Amazon calls working backwards—starting with the customer-facing outcome and reverse-engineering the path to get there.

Marcelo explains:

“It’s a vision and strategy document. Not a plan. You’re imagining the future and asking: what must be true for this to succeed?”

By avoiding premature execution details, teams stay focused on the big picture. Including implementation specifics too early can derail strategic discussions.

The Lifecycle of a PR FAQA PR FAQ typically takes one to two weeks to develop. It begins with a draft from a project owner (like a CTO or product lead), followed by iterative review sessions with stakeholders. These reviews aren’t just approvals—they’re collaboration points to refine the vision.

Eventually, the document reaches a decision meeting. The result: go, no-go, or revisit later.

Once approved, the PR FAQ becomes the team’s north star. And unless a major strategic assumption changes, the document rarely needs rewriting.

“The value is clarity. Everyone knows why the project matters—and can make aligned decisions without constant check-ins.”

When Should You Use One?Marcelo emphasizes that PR FAQs aren’t only for product launches. You can use them for:

  • Migrating vendors (e.g. switching observability tools)
  • Strategic tech shifts (e.g. adopting Snowflake)
  • Internal programs or process overhauls
  • Even writing a book (Marcelo used one for his!)

The key isn’t the size of the task—it’s the impact. A one-day feature with major business implications might deserve a PR FAQ. A three-month refactor that introduces legal or compliance risks might need one too.

OKRs + PR FAQ = A Healthy StackMany teams use OKRs (Objectives and Key Results) for planning—but Marcelo warns that OKRs alone can lack context.

“We’d spend 10–15 minutes in meetings just explaining what the OKR meant. Eventually, we started writing a paragraph of narrative. At Amazon, I realized: that’s the PR FAQ.”

Marcelo sees PR FAQs as a perfect precursor to OKRs. They bring clarity before planning, and help teams avoid setting vague or misaligned objectives.

Common Pitfalls to AvoidMarcelo shares three mistakes teams often make when adopting PR FAQs:

  1. Confusing strategy with execution
    Don’t list features or roadmaps. Stay focused on why.
  2. Using vague or “salesy” language
    Be specific. Say “78% of users used this feature last week” instead of “most users love it.”
  3. Treating it as a solo activity
    A PR FAQ isn’t a manifesto—it’s a collaborative document. Use it to pull your team in, not push ideas out.

Want to Try It?Marcelo’s book, The PR FAQ Book, is a practical guide filled with real examples, templates, and tips for using the framework in your own team.

🎁 Join the giveaway for Semaphore Uncut listeners: theprfaq.com/giveaway/semaphore-uncut

The post Marcelo Calbucci on Strategic Product Thinking appeared first on Semaphore.

View Details

What’s NewIn this release, we’ve added two major features:

  • Markdown Reports: Render markdown files directly on the workflow and job pages.
  • Git-Agnostic Integration: Connect any Git server via a simple push-hook script.

For more details on other changes, head over to the changelog.

📋 Markdown ReportsThe new Reports tab allows you to create rich, custom markdown reports to enhance and expand the UI of your job and workflow pages. This feature helps you add tailored visualizations, summaries, and detailed information without leaving Semaphore, making it easier to communicate build results and pipeline insights.

  • New “Reports” tab: View .semaphore/REPORT.md files directly in the workflow or job pages, fully rendered for easy reading.
  • Mermaid.js support: Visualize your pipeline with dynamic charts and diagrams using Mermaid.js.
  • Easily generate reports: The test-results CLI has been expanded to help you generate basic job reports quickly.
  • Fully customizable: Add test results, deployment notes, and other useful content to better organize and communicate your CI/CD process.

This feature helps teams present their pipeline results in a more meaningful way, making it easier to share insights and keep everyone on the same page.

🛠️ The test-results CLI UpdateThe test-results CLI has been updated with two new commands, making it even easier to generate detailed, insightful reports directly from your jobs.

  • test-results command-metrics REPORT.md: Generates a timeline chart showing the execution time for each command in your job, and appends it to your report for a clear, visual representation of your job’s performance.
  • test-results resource-metrics REPORT.md: For teams using Semaphore Cloud machines, this command pulls resource usage metrics (CPU, memory, disk) and displays them as neat charts in your report.

🔗 Git-Agnostic IntegrationSemaphore now supports integration with any Git server.

  • What’s new: You can use a post-receive script to connect to any Git server, whether it’s a self-hosted Git instance, or another provider.
  • How it works: After configuring the script, Semaphore can automatically trigger builds from pushes to your Git server — just like it does with GitHub, Bitbucket and GitLab.
  • Why it matters: This adds flexibility, so teams using custom or self-hosted Git setups can use Semaphore without switching services.

This change removes any vendor lock-in and expands the range of Git providers you can work with.

👥 External Contributor ShoutoutWe’d like to give a special thanks to @colemickens from Determinate Systems for their contribution in extending and expanding our OIDC Token functionality. Your efforts helped make this release even better!

If you are a Nix fan, be sure to check out Determinate Systems.

🔮 What’s Coming in v1.3In the upcoming v1.3 release, we’ll be focusing on both improving the user experience and making it easier to contribute to Semaphore.

  • Branch Page & Workflow List UX Improvements: We’ll be enhancing navigation and pagination, along with adding useful filters like a datetime filter to help you quickly find specific workflows from the past.
  • Contributor Experience: We’re working on improving how contributors interact with Semaphore. This includes:
    • Better documentation of our architecture and services to help you understand how everything fits together.
    • Expanded contribution guide with more details on how to get involved.
    • Local testing solution: We’re investigating ways to allow you to test Semaphore changes locally before pushing them.

These updates aim to make Semaphore even easier to use and contribute to, whether you’re managing workflows or helping build the platform itself.

The post Semaphore v1.2 Release Roundup: Reports and Git Agnostic appeared first on Semaphore.

View Details

At Semaphore, we’ve been rethinking our public API from the ground up.

Over the years, as our platform grew, so did the complexity of our API infrastructure. We tried multiple approaches—some promising, others not so much. The result? A fragmented developer experience that made it harder to integrate, extend, and maintain.

So we decided to change that.

This post is about our journey to unify the Semaphore API behind a single, resource-oriented design. It’s about making things simpler, more consistent, and easier to use—not just for us, but for every developer who builds on Semaphore.

Where Things Got MessyOur API has evolved in layers. At one point or another, we were running:

  • gRPC APIs that exposed through a reverse proxy, translating HTTP to gRPC.
  • Per-service HTTP APIs which were maintained independently.
  • A dedicated HTTP server translating requests into internal service calls.

Each method solved problems in its own silo. But together? They made our system hard to reason about and even harder to scale.

The Turning Point: A Dedicated API GatewayThe most promising approach was the dedicated HTTP gateway: a single service that receives external HTTP requests and translates them into internal gRPC calls.

This gave us several key advantages:

  • A single point for applying authentication, validation, and formatting
  • One source of truth for the request/response payloads
  • Easier maintenance as new features are added
  • Consistency in how internal and public APIs communicate

It was such an obvious choice that we leaned in.

Resource-Oriented API DesignTo make the API intuitive and extendable, we adopted a resource-oriented design. Inspired by Kubernetes and REST best practices, our resources now follow a consistent structure:

{ "kind": "project", "metadata": { "id": "proj-123" }, "spec": { "name": "My App", "description": "CI/CD pipeline" }} * kind tells you what type of resource you’re working with. * metadata includes identifiers and timestamps. * spec holds the actual configuration or desired state.

This pattern makes it easier to reason about objects across the system and gives us room to grow the API without breaking clients.

Richer RepresentationsAnother upgrade we made during design is to include rich object representations of objects instead of their raw IDs.

So, for example, instead of:

"created_by": "user-456" You now get:

"created_by": { "kind": "user", "id": "user-456", "name": "amir"} This gives consumers more context without extra API calls and sets us up for future extensibility.

Supporting Actions Beyond CRUDNot all operations fit neatly into create/read/update/delete actions. For example, starting a workflow requires more than just updating a resource.

So we introduced custom methods for resource-specific actions:

POST /projects/{project_id}/workflows/{id}/start These methods are scoped to the resource, follow predictable naming conventions, and include actionable error messages when things go wrong.

Built with OpenAPI (and Built to Last)A major part of this overhaul was committing to OpenAPI-first development.

Each endpoint in the API is defined with:

  • Typed request and response schemas
  • Collocated OpenAPI specs
  • Automatic validation and permission checks
  • Clear documentation generated from the source

This keeps our docs accurate, reduces drift, and makes the API easier to contribute to and extend.

Inside the Project StructureHere’s how everything fits together under the one API:

  • Handlers: we use one file per endpoint. Each files handles casting, validation, logging, permissions, and forwarding requests.
  • Internal clients: we use them as bridges between handlers and gRPC services.
  • Formatters: we use them to translate between internal requests and external responses.

We’ve designed things to be modular and clear, so contributors can get up to speed quickly. It’s now easier than ever to add a new endpoint or update an existing one.

Want to Get Involved?The Semaphore API is open source, and we’d love your feedback.

If you’re a developer who:

  • Wants to help improve a real-world API
  • Has ideas for how to make things clearer or more consistent
  • Likes working with Elixir (or wants to learn!)

Then head over to our GitHub repo, explore the public_api_v2 project, and jump into the issues or discussions.

We’ll also be publishing RFCs for upcoming API design decisions—keep an eye out.

What’s NextHere’s what’s coming soon:

  • Better error handling across all endpoints
  • More complete OpenAPI coverage
  • Continued unification of all service interactions behind the gateway
  • Easier ways to discover and explore available resources

We’re far from done, but we’re on a solid path—and the developer experience is already better than it was just a few months ago.

Final ThoughtsRebuilding an API isn’t glamorous work. It involves digging into edge cases, rethinking old assumptions, and making a lot of small decisions that add up to a big difference.

But the result? A cleaner, more consistent, and more enjoyable developer experience.

Check out the full talk from Semaphore Summit on YouTube if you want to dive deeper into the architecture and see the code in action.

Thanks for reading—and we hope to see you in the pull requests.

The post One API to Rule Them All: Building a Cohesive API for Semaphore appeared first on Semaphore.

View Details

AI and Large Language Models (LLMs) have entered our lives and jobs in the last few years, helping us simplify and automate tasks and processes.

For IT professionals, AI is helping streamline workloads, speed up development cycles, implement new features faster, and reduce bugs and errors. But this is not all that AI can do for us.

Managing technical debt is a task that requires time and effort from developers, architects, and managers, but it is also a task where AI can be of great help.

In this article, you will learn:

  • What technical debt is.
  • How AI can help you reduce technical debt.
  • Abilities and frameworks that can help you manage your technical debt.
  • Challenges and limitations of using AI to reduce technical debt.

Let’s dive into it!

What is Technical Debt?Technical debt refers to the cost incurred when choosing short-term solutions over long-term ones. It happens when we make quick fixes instead of investing more time and resources in improving the overall project we are working on.

There are different types of technical debt. Or, more precisely, technical debt comes with different costs, which can be classified at a high level as follows:

  • Code debt: This type of technical debt occurs when you write code without considering its maintainability, readability, and scalability. Typical examples include spaghetti code, lack of modularity, hardcoded values, or insufficient use of design patterns. Many times, even if you wrote excellent code for this particular feature/problem set, the business moves on (think of a bug report which is actually a feature request) but the code did not. sigh
  • Documentation debt: Let’s be honest: this is one of the most common forms of technical debt. It occurs when documentation for code, APIs, systems, or processes is missing, outdated, or incomplete, making it difficult for others to understand and contribute effectively to a project they don’t know much about. And even though it’s “easier” (less time intensive) to write docs, you’re always better off writing test cases (which at least have a chance of letting you know if/when the business has moved on but your code has not).
  • Testing debt: When you skip writing tests or fail to maintain existing test suites, you create testing debt. This leads to increased risk during software releases and makes it harder to identify and fix issues promptly. Insufficient or missing automated tests lead to poor test coverage and increased risk of bugs.
  • Architecture debt: This form of technical debt occurs when architectural decisions made early in the project lifecycle lead to suboptimal designs that become increasingly challenging to modify or extend over time. Common cases include monolithic systems that should be modularized, poor database design, or lack of microservices where appropriate.
  • Infrastructure debt: This kind of debt arises when infrastructure components such as servers, databases, networks, or cloud services are not properly designed, maintained, or scaled to meet evolving needs. Common scenarios include manual deployment processes, lack of CI/CD pipelines, or reliance on legacy hardware.
  • Security debt: Security debt is arguably the most dangerous kind of technical debt because it can lead to serious consequences if left unaddressed. It results from neglecting security best practices, inadequate threat modeling, or failing to implement proper access controls and data protection measures. This can expose sensitive information and compromise system integrity.

If you’ve ever worked on a legacy codebase or an old project, you know what technical debt means—and I know you don’t like it at all.

However, if you want to be completely honest, you have to admit that every line of code you write is potentially technical debt. You’d like that line of code to stay there as it is forever, but the reality is that it won’t happen. So why not test it now? Why not create living tests immediately? These tests not only ensure your code works as expected but also serve as the best form of documentation for future developers who will work on this codebase.

Well, the speed of development processes today is so important that we often find ourselves in situations where we need to deliver something quickly and efficiently. Sometimes, this means sacrificing some quality aspects of our codebase, unfortunately. However, this doesn’t mean that we shouldn’t try to minimize these sacrifices and keep our codebase healthy and well-maintained. The impact of technical debt can be severe in the long run, affecting companies in different areas, such as:

  • Reduced development speed: Today, you’re running fast to deliver new features while accumulating technical debt because you don’t document your code and have low test coverage. Tomorrow, you’ll spend hours debugging problems caused by your previous actions and lose precious time trying to figure out how things work again. This will slow down the development of new functionalities as you’ll need to work around existing issues, such as messy code, poor architecture, or lack of automation.
  • Increased maintenance costs: As mentioned before, technical debt increases the complexity of maintaining and updating the codebase. This can result in higher ongoing expenses due to longer development cycles, more frequent bug fixes, and potential rework required to address underlying issues.
  • Decreased product quality: Technical debt often results in more bugs, performance issues, and system failures, which can degrade the overall quality of the product. This can also mean that users may encounter unexpected behavior or crashes, leading to negative user experiences, decreased customer satisfaction, and an increase in support tickets.
  • Delayed time-to-market: The longer it takes to develop and release new features, the slower your company can respond to market demands and capitalize on opportunities. This delay can put you at a competitive disadvantage compared to competitors who invest in continuous improvement and innovation.
  • Increased risk of security breaches: Poorly managed technical debt can introduce vulnerabilities and security risks into your application. If attackers exploit these weaknesses, they could gain unauthorized access to sensitive data, disrupt operations, or cause financial losses.

How AI Can Help Reduce Technical DebtThe major difficulty of managing technical debt is that it is created daily with every line of code, so it requires a continuous effort to monitor and improve the codebase. This means you should have a process that constantly reviews and refactors code, updates documentation, adds and improves tests, verifies that security has been managed to address new issues, and ensures that everything works smoothly together.

It’s hard, I know.

The good news is that AI can help you reduce technical debt. In particular, AI coding assistants can be integrated into your workflow and help you reduce technical debt by operating in the following areas:

  • Code analysis and refactoring: AI-powered abilities can analyze your codebase and provide suggestions for improvements, such as identifying unused variables, optimizing algorithms, or suggesting better naming conventions. These recommendations can help you clean up your code and eliminate unnecessary technical debt.
  • Automated testing: AI assistants can generate test cases based on your codebase, ensuring comprehensive coverage across various scenarios. This saves time and effort while minimizing the risk of introducing new defects through manual testing.
  • Documentation generation: AI assistants can create technical documentation by extracting information from your code comments and source files. A typical example is the generation of documentation for functions, classes, or modules.
  • Security assessment: Some AI abilities (like DeepCode – now part of Snyk) are specifically designed to scan your codebase for potential security vulnerabilities and suggest remediation strategies. This way, you can mitigate the risk associated with technical debt related to insecure coding practices, including detecting outdated or vulnerable dependencies.

In other words, AI can help you reduce technical debt in real time by integrating AI coding assistants into your workflows. This is revolutionary because it allows you to catch technical debt as soon as possible and prevent it from becoming a bigger problem later on.

Tools for AI-Driven Technical Debt ReductionIn a recent post, we reviewed the best AI coding assistants for 2025.

This is the chance to expand more this list to give you more options to choose your AI coding assistant to help you reduce technical debt:

  • Cline: Powered by Claude 3.7 Sonnet’s agentic coding capabilities, Cline is an AI assistant designed to operate within IDEs, assisting you with software development tasks. Now able to use “dynamic thinking”, start your conversation off with the assistant in “Plan” mode and, once satisfied with the course of action, switch over to “Act”. It will then autonomously generate and edit code, execute terminal commands, and browse the web, all while requiring user permission for each action. Learn how it works here.
  • Tabby: Tabby is an open-source, self-hosted AI coding assistant, compatible with major coding LLMs like CodeLlama, StarCoder, and CodeGen. Its code completion engine is designed to understand your coding context and provide real-time suggestions that are accurate and relevant. As an open-source solution, it ensures software supply chain safety, giving you peace of mind. Learn how it works here.
  • Cursor: Cursor is an AI-powered coding assistant with the primary goal of streamlining the coding process and improving overall code quality. It integrates seamlessly with IDEs, provides a chat that lets you talk with an AI that sees your codebase, and can get up-to-date information from the internet. Learn how it works here.
  • Codiga: Codiga is an AI-powered coding assistant and static code analysis ability designed to help developers write cleaner and safer code. One of its most appreciated features is its ability to perform real-time static code analysis, continuously checking for bugs, security vulnerabilities, and inefficiencies as you write code. Learn more about Codiga here.
  • IntelliCode: IntelliCode, developed by Microsoft, is an AI-assisted code completion ability built into Visual Studio and Visual Studio Code. Its underlying LLM is trained on thousands of high-quality repositories from GitHub with high star ratings, allowing it to provide recommendations based on best practices and common patterns observed across the development community. Learn more here.

Challenges and Limitations of Using AI for Technical DebtAs you’ve learned throughout this article, AI offers significant advantages in managing technical debt. However, it doesn’t come without challenges and limitations.

Understanding these can help you make informed decisions when integrating AI into your workflows.

Accuracy and Reliability of AI ModelsFirst of all, you have to face the truth: AI models are not infallible. They can produce false positives or negatives during code analysis, flagging issues that don’t exist or missing critical problems entirely. Additionally, AI often struggles to fully understand the context of your codebase, especially in complex or domain-specific projects and this can lead to suggestions that are irrelevant or even counterproductive, requiring careful review by developers.

So, your critical skills are still invaluable. You can’t just turn off your brain and let AI do everything for you. You have to evaluate the suggestions provided by AI and decide whether they are useful or not.

Integration with Existing WorkflowsIntegrating new tools or processes into already existing workflows is not always easy, and this also applies to AI coding assistants.

So, ensuring seamless integration between AI and your (and your team’s) current abilities, processes, and team dynamics requires time and effort. Moreover, while automation can save time, it’s essential to strike a balance between relying on AI and maintaining human oversight to ensure quality and alignment with project goals so that you are still in charge of your project objectives, meaning: you don’t just blindly delegate everything to AI.

Ethical and Security ConsiderationsAI-generated code must be carefully reviewed to ensure it adheres to security best practices. While some AI coding assistants are specifically crafted with security in mind, not all of them provide a high level of security when offering suggestions. So, overlooking this step can introduce vulnerabilities into your system, potentially leading to serious consequences.

Additionally, over-reliance on AI can create a dependency that may hinder your team’s ability to solve problems independently or innovate effectively — and this is another reason why you should never rely solely on AI for decision-making.

ConclusionIn this article, you’ve learned how AI can help you reduce technical debt. You’ve seen how AI coding assistants can assist you in analyzing your codebase, generating tests, creating documentation, assessing security, and more. You’ve also explored the benefits of integrating AI into your workflows and discussed the importance of balancing automation with human oversight.

However, remember that AI is not a magic wand that solves all your problems. It requires careful consideration and implementation to achieve optimal results. In other words: your skills are still needed and highly valuable.

The post How to Use AI to Reduce Technical Debt appeared first on Semaphore.

View Details

This article is part of our build-in-public series on Semaphore Backstage, where we share how we’re designing and building our new open-source CD workflows feature, Canvas.

From YAML to Canvas: A New Way to Ship SoftwareCI/CD pipelines are essential to build and test your software. But complexity is always increasing, and once your app spans multiple services, environments, and approval steps, your deployment logic can get hard to reason about.

That’s the challenge Canvas solves.

Semaphore Canvas is a new visual way to design, track, and automate your deployments across clouds, environments, and teams. It’s a live, interactive diagram of your delivery workflow.

It builds on top of your existing Semaphore pipelines, giving you a new layer to manage:

  • Complex multi-stage rollouts
  • Queued versions with manual or automatic approvals
  • Dynamic promotion paths
  • Run and failure history per environment

Whether you’re deploying nightly RCs or promoting releases from staging to prod, Canvas helps you ship with confidence and visibility. It’s the bridge between developer automation and human decisions.

Why We’re Building a Visual Deployment DesignerDevelopers already use Semaphore to define CI/CD workflows in YAML. But when your software grows across services and environments, reasoning about your delivery logic in code alone becomes limiting. You end up with interdependent pipelines, environment-specific rules, manual approvals, rollback logic, and more.

We wanted to make this complexity visible and manageable. That’s what Canvas is for.

Canvas lets you:

  • Model your deployments visually
  • Chain stages together based on events
  • Add manual approvals and conditions
  • Track runs and queue history
  • Understand which version is where

As always, you’re still setting up jobs and pipelines with the visual editor. But now, you can orchestrate them using a visual interface that mirrors how your team thinks about environments and release flows.

Want to deploy to QA, then trigger staging and production only when tests pass? Want to auto-clean test environments after three hours? Want to roll back a release candidate that failed validation in AWS but passed on GCP?

With Canvas, you can.

Managing Real-World Deployments — Queues, Approvals, and RollbacksWe designed Canvas to reflect how teams ship software — iteratively, carefully, and often across many clouds.

Let’s say you’re releasing RC versions of your app. With Canvas, you can model a testing flow like this:

  • A new GitHub release triggers a deployment to a DigitalOcean test instance
  • The version enters the queue and waits for your manual approval
  • You test the deployment manually
  • Once you’re satisfied, you approve it for Google Cloud and AWS
  • If something breaks, you can reject, rerun, or rollback the queued item

Each environment becomes a stage. Each version that flows through it is a queued item. And Canvas lets you manage it all visually.

More advanced use cases are supported too:

  • Selective deployments — Skip RC versions and only promote stable builds
  • Parallel targets — Fan out from a single stage to deploy to multiple cloud targets
  • Approval chains — Add manual gates, per-stage conditions, or fallback paths
  • Timeout logic — Auto-delete test clusters three hours after creation

And Canvas keeps your audit trail clean: every run, every approval, and every rejection is tracked and timestamped.

Building Canvas in Public with React Flow and AI PrototypingWe’re not just building Canvas — we’re building it in public.

To prototype the UI, we skipped the usual wireframing tools. Instead, we paired up with AI and started coding directly with React Flow, a powerful graph visualization library for React.

We used an AI programming assistant to:

  • Scaffold the project structure
  • Create reusable node types (e.g. stages, event sources)
  • Add interaction logic and state management
  • Style nodes, edges, and queues based on state (e.g. running, failed, approved)
  • Implement interactive sidebars with run history, current status, and queue items

The result? A living prototype — where we could click, test, and explore real deployment logic.

This helped us:

  • Move fast without boilerplate
  • Iterate on real feedback from our team
  • Share progress during internal demos
  • Avoid building the wrong thing early on

It also gave us confidence that the final UI would feel intuitive for developers, not like a bolt-on abstraction.

Looking AheadCanvas is already changing how we reason about deployments — and we’re just getting started.

If you want to follow our progress:

  • Subscribe to the https://www.youtube.com/@SemaphoreBackstage
  • Watch us build features in real time
  • Share your feedback — we’d love to hear how you’d use Canvas

The post Introducing Semaphore Canvas: Visual Workflow Automation for Deployments appeared first on Semaphore.

View Details

We’re excited to announce that Semaphore now offers Ubuntu 24.04 as one of our available build images. Starting this week, you can take advantage of the latest Ubuntu LTS release on Semaphore to run your CI/CD pipelines.

What’s NewThe Ubuntu 24.04 image ships with updated versions for all the software you love, including:

  • Docker 28
  • Updated runtimes for all languages
  • Updated SDKs for cloud providers
  • Newer versions of browsers for end-to-end testing
  • Extended support and the latest patches

You can find all the details on our Ubuntu 24.04 image in the docs.

What’s Changing for You New OS image: Ubuntu 24.04 is now available alongside our existing images. * Non-breaking update*: There are no breaking changes—your existing pipelines will continue to run exactly as before.

Impact on Your PipelinesSince this isn’t a disruptive change, your current workflows will not be affected. The main advantage is simply having access to the latest Ubuntu LTS version, with all its updated packages, improved performance, and extended security support.

How to Use Ubuntu 24.04To start using Ubuntu 24.04:

  1. Open your project’s pipeline configuration.
  2. Select a pipeline
  3. Under Agent select “Linux Based Virtual Machine”
  4. Select either one of the e2 or f1 machine types
  5. Select ubuntu2404

Alternatively, you can change the agent on the pipeline YAML directly. For example:

version: v1.0name: CI Pipelineagent: machine: type: f1-standard-2 os_image: ubuntu2404 Commit and push the changes. Your next pipeline run will execute on Ubuntu 24.04.

F1 & E2 Machines: Rock-Solid PerformancePlease note that the Ubuntu 24.04 image is currently supported only on our high-performance e2 and f1 machines. Both machine types deliver:

  • e2 machines: Cost-effective builds with great concurrency and reliability.
  • f1 machines: Top-tier speed, ideal for CPU-intensive tasks and faster feedback loops.

If you haven’t tried f1 machines yet, now’s a perfect time to see what they can do for your build times!

Looking Ahead: Ubuntu 20.04 DeprecationUbuntu 20.04 reaches official EOL on 31 May 2025. We plan to deprecate our 20.04 image shortly thereafter and will publish a follow-up announcement well in advance to help you migrate. No action is needed right now—just stay tuned for that future notice.

Thank you for choosing Semaphore! We’re committed to keeping your CI/CD environment up to date and performant. If you have any questions or feedback about the Ubuntu 24.04 rollout, please reach out to support or our post your questions on our Discord server.

The post Semaphore Now Supports Ubuntu 24.04 appeared first on Semaphore.

View Details

Semaphore is a robust and efficient CI/CD platform designed to streamline software delivery. Whether you’re a solo developer or have a large team, it helps automate builds, tests, and deployments effortlessly. While it appears straightforward on the surface, its underlying architecture is a sophisticated system composed of over 30 microservices, various communication protocols, and a strong infrastructure to ensure seamless pipeline execution.

In this article, we’ll explore the core architecture of Semaphore, break down the CI/CD process from code push to execution, and examine how developers interact with the system.

To find out more, you can listen to the podcast episode above or watch the video. Enjoy!

Understanding Semaphore’s ArchitectureSemaphore balances scalability with ease of use, making it an ideal CI/CD solution for teams of any size. Its architecture is built on a distributed system that ensures high performance, fault tolerance, and automation. Let’s explore the core technologies that power Semaphore’s reliability, scalability, and efficiency.

Core TechnologiesSemaphore is powered by Elixir, which is known for its ability to handle massive concurrency and fault-tolerant systems, making it well-suited for orchestrating thousands of CI/CD jobs. We also leverage Go for performance-critical services and are gradually replacing legacy Ruby components with Elixir. The UI layer uses JavaScript & HTML for seamless developer interaction.

  • Elixir: Ideal for building highly concurrent, distributed, and fault-tolerant systems that can efficiently manage thousands of jobs in parallel.
  • Go: Used in performance-critical areas where efficiency is paramount.
  • Ruby: A legacy component being rewritten in Elixir to align with the rest of the stack.
  • JavaScript & HTML: Used for the presentation layer.

How Semaphore Services Talk to Each OtherSemaphore services need to communicate efficiently to process jobs without delay. External interactions happen over HTTP, while gRPC ensures lightning-fast internal messaging. For background tasks, RabbitMQ keeps everything running smoothly behind the scenes.

  • gRPC: Provides high-performance internal communication with strict service contracts.
  • Proto Files: Define service contracts before implementation to ensure smooth integration.
  • RabbitMQ: Enables asynchronous messaging, allowing background tasks to run efficiently.

Scalable and Resilient InfrastructureSemaphore is built to dynamically handle any workload. Kubernetes orchestrates and scales resources as needed, PostgreSQL stores pipeline and job data efficiently, Redis improves performance with caching, and RabbitMQ ensures smooth communication between services.

These components work together to ensure that Semaphore handles workflows smoothly, balancing speed and reliability. Now, let’s dive into how Semaphore processes CI/CD jobs.

The CI/CD Workflow in SemaphoreBefore we dive into the mechanics, let’s break down Semaphore’s two main workflows:

  1. CI/CD Processing – The backbone of Semaphore, ensuring every code change is built, tested, and deployed.
  2. CI/CD Management – Where users interact with Semaphore to configure environments, manage projects, and control access.

How Semaphore Runs Your CI/CD WorkflowsWhen you push a new commit, Semaphore immediately takes action to ensure a smooth build, test, and deployment process:

  1. Hook Processing: Semaphore detects the commit, verifies authenticity, and prepares it for execution.
  2. Pipeline Processing: The Plumber Service reads the YAML configuration, organizes dependencies, and orchestrates job execution.
  3. Job Execution: The Zebra Service assigns jobs to available agents, tracks execution, and ensures tasks complete on time.
  4. Notifying External Systems: Semaphore updates integrations like Slack or GitHub to keep you informed.

Workflow Steps1. Hook ProcessingFirst, Hook Processing begins by listening for incoming events. The Hook Receiver accepts webhooks from Git repositories and verifies authenticity, while the Hook Processor enriches the data, converts it into an internal format, and stores it for traceability.

2. Pipeline ProcessingOnce a webhook is validated, Semaphore interprets the pipeline definition and orchestrates execution.

  • Plumber Service: Defines the pipeline structure based on the YAML configuration, ensuring proper sequencing of jobs.
  • Dependency Management: Ensures jobs run in the correct order and do not execute prematurely.

3. Job ExecutionNext, the job executes. The following components make this happen:

  • Zebra Service: Assigns jobs to available agents, gathers necessary resources, and tracks job states.
  • Agents: Execute jobs with the necessary environment and configurations.
  • Timeout Management: If a job exceeds the execution time, Zebra forcefully terminates it to prevent queue blocking.

4. Notifying External SystemsOnce execution completes, Semaphore updates external systems:

  • Notification Service: Sends real-time updates via Slack or webhooks.
  • GitHub Notifier: Updates commit statuses in the version control system.

Additional Job Execution MethodsBeyond code pushes, developers can:

  • Rerun Pipelines: Re-execute workflows without new commits.
  • Start Debug Jobs: Run specific jobs in isolation for debugging.

Managing Your CI/CD Environment in SemaphoreBeyond executing jobs, Semaphore gives developers full control over their CI/CD workflows, including environment configurations, user permissions, and system integrations. Let’s dive into how you can optimize your setup.

User Interaction with SemaphoreUsers interact with Semaphore through the UI and API, with HTTP handling external communication and gRPC managing internal services.

Keeping Semaphore SecureSemaphore ensures security at every step. Keycloak handles authentication, verifying user identities before granting access. RBAC (Role-Based Access Control) enforces fine-grained permissions, ensuring only authorized users can modify configurations or trigger builds.

Wrapping UpSemaphore is designed to simplify CI/CD without sacrificing power. With automated pipelines, secure access control, and a scalable microservices architecture, it enables developers to ship high-quality software faster.

Now that you have a clearer picture of Semaphore’s inner workings, why not try it out? Whether you’re new to CI/CD or an experienced user, there’s always something to explore. As an open-source project, Semaphore welcomes contributions from the community to continuously improve and enhance its capabilities.

🚀 Ready to get started? Try Semaphore here or contribute to our open-source project and help shape the future of software delivery!

The post Unveiling the Semaphore Architecture: How It Works Under the Hood appeared first on Semaphore.

View Details

While AI-generated code and copilots have become commonplace, their role in DevOps and infrastructure remains less defined. The tooling is improving rapidly, but the risks—from non-determinism to lack of visibility—make AI adoption in production a complex, evolving journey.

In this episode of Semaphore Uncut, Patrick Debois—Generative AI and DevOps specialist —joins Darko to share his perspective on how AI intersects with DevOps, DevSecOps, and infrastructure as code. Patrick discusses everything from generative tooling to failure handling, and what makes this era of automation both exciting and risky.

A Career Built on CuriosityPatrick’s journey through the software world has always been guided by a drive to learn. He’s worn almost every hat imaginable—tester, sysadmin, mobile developer, VP of Engineering—and that broad experience led him to the insights that sparked the DevOps movement.

What set Patrick apart early in his career was his desire to work across domains, building empathy between teams and solving the friction points he observed first-hand. That perspective would later shape the DevOps Handbook, which he co-authored alongside Gene Kim, Jez Humble, and others.

Although Patrick says most of the writing credit goes to his co-authors, many of the core ideas came from his real-world experiences and the early DevOpsDays community. The second edition, with contributions from Dr. Nicole Forsgren, introduced more data and up-to-date enterprise stories, but the foundational patterns remain relevant—even if the landscape has continued evolving.

From Virtual Production to AI in DevOpsPatrick’s entry into the AI world didn’t begin with infrastructure. During the pandemic, he started exploring virtual production—automating video and media workflows using game engines like Unreal Engine. That interest in automation and digital characters led him to experiment with generative AI, initially for rendering and voice generation.

Then ChatGPT landed, and suddenly AI had a meaningful place in software development again. Patrick was hooked. He began integrating LLMs into developer workflows, helping engineers at his company use AI tools effectively, and later started advising other companies on building more robust AI-powered systems.

What keeps him engaged? “Every day, I’m learning something new—and it hasn’t stopped for a year and a half.”

Infrastructure as Code Meets Generative AIWhile AI-generated code has caught on quickly in application development, Patrick is most intrigued by its impact on infrastructure as code and DevOps workflows.

Because IaC languages like Terraform are more structured and domain-specific than general-purpose programming languages, they present an interesting target for generation. Tools can now create entire Terraform environments, visualize them, and even highlight only the parts relevant to a given change—helping reduce cognitive load.

But the bigger question is validation.

“Anyone can generate Terraform,” Patrick says, “but how do you validate it? How do you ensure it aligns with your organization’s security and compliance rules?” Patrick points to rule-based systems and emerging standards like .aiconfig files as a way to encode domain-specific requirements into the generation and review process.

Still, challenges remain. Much of the validation burden is still on the developer, and the feedback loops between code and production are often disconnected. Patrick sees a gap between developer tooling and production observability that AI could help bridge—but hasn’t yet.

From Testing to Chaos Engineering for AIPatrick draws a compelling analogy between DevOps maturity and where AI is today.

In DevOps, teams moved from simple test automation to full CI/CD pipelines, observability, resilience engineering, and chaos testing. The AI lifecycle may follow a similar arc—from basic completions to robust feedback loops that simulate failure and enable automated recovery.

“Only about 20% of the work is automation,” Patrick says. “The other 80% is preparing for failure.”

He also emphasizes the need to teach AI systems to test themselves—or at least to validate outputs using other models, digital twins, or sandboxed environments. In incident response, for instance, Patrick envisions systems that don’t just suggest solutions, but actually try multiple fixes in parallel and report back.

The Human-AI Feedback LoopOne area Patrick finds fascinating is how AI affects engineering culture and team dynamics. As AI takes on more code generation, the role of engineers shifts toward reviewing, validating, and integrating. Juniors may be able to ship code faster, but seniors are more essential than ever to evaluate quality and ensure safety.

What’s missing, Patrick says, is tooling that supports the review process itself. He points to tools like Cursor, which offer multiple ways to review code changes—including text explanations and focused visual diffs—as the future of developer experience.

“If you always see the full picture, you lose focus. AI can help us zoom in on what matters.”

The Future of Observability & AI-Native ToolingWhile DevOps has long relied on tools like Grafana and Prometheus, there’s still no AI-native operator capable of monitoring logs and dashboards in real time and proactively suggesting or testing fixes. Patrick is hopeful—but realistic.

“I can’t wait for those tools to exist,” he says. “But we’re not there yet.”

He expects progress to come from more specialized models trained on domain-specific data (like Grafana dashboards or logs), rather than general-purpose screen readers. Generative BI tools and prompt tracing are promising steps, but the field is still emerging—and often fragmented.

Final Thoughts: Why It’s Still Day OnePatrick believes we’re only at the beginning of understanding how generative AI will impact DevOps, infrastructure, and developer workflows. While the technology is advancing quickly, organizational change takes time—and toolchains still feel disjointed.

His advice? Get hands-on.

“I call it method acting. I have to use the tools myself. I need to feel the pain points firsthand.”

That hands-on mindset has guided Patrick through every major shift in software—and it’s what keeps him excited about what comes next.

Follow Patrick Debois

📺 YouTube: Jedi Forever
💼 LinkedIn: Patrick Debois
🐦 Twitter (X): @patrickdebois

The post Patrick Debois on AI & DevOps: What’s Next? appeared first on Semaphore.

View Details

Artificial intelligence continues to evolve rapidly, giving rise to AI-driven “agent” frameworks that can autonomously handle tasks and communicate with APIs in a human-like manner. Unlike typical automation scripts, AI agents leverage large language models (LLMs) to interpret, learn, and make context-based decisions. They take on repetitive chores—like analyzing new pull requests or generating release notes—so developers can focus on what truly matters.

Why AI Agents Over Traditional Automation?Traditional automation scripts are often static, following predefined if-else rules or workflows. By contrast, AI agents adapt and refine their own instructions, making them more flexible. For example, an agent configured to detect potential security risks in PRs can learn from new vulnerabilities or updated code patterns, rather than being limited to a rigid set of checks.

Common Developer Tasks to Automate Pull request reviews: Summarize diffs, catch style issues, and highlight security risks. * Release notes: Parse commit messages for automatically generated release content. * Code reviews and triaging: Suggest improvements, identify best practice violations, and group user-reported issues by root cause. * Debug log analysis*: Surface patterns in log data and proactively recommend pipeline optimizations or environment fixes.

Project Goals and Expected OutcomesSet up an AI agent that responds to triggers like new commits and integrates with Semaphore’s CI/CD pipeline to automatically summarize and review the changes.

Setting Up the Project and Choosing Your AI Agent ToolsKey Tools and FrameworksYou have several options for building an AI agent:

  • AgentGPT & AutoGPT: Out-of-the-box “autonomous” GPT-driven frameworks.
  • LangChain: Comprehensive toolkit for chaining LLM prompts and external APIs.
  • LlamaIndex: Data-centric approach to building context/query structures on top of LLMs.
  • Vercel AI SDK: Streamlined developer experience with built-in tooling for prompt orchestration, plus integration with providers like OpenAI.

Each solution has strengths: AgentGPT/AutoGPT for faster prototypes, or lower-level libraries like LangChain and the Vercel AI SDK for robust customization.

Version Control, Plugins, and Security MeasuresKeep your AI agent code in a dedicated repository or a separate folder in your main codebase. Adhere to best practices such as branching for feature work, small commits, and meaningful commit messages. This ensures that your automated tasks remain transparent and easy to maintain.

Whether you’re linking to GitHub, Slack, Jira, or other services, store credentials securely (e.g., in Semaphore secrets). Give your AI agent the minimal required permissions—like read-only on certain repos or specific Slack channels—so you don’t compromise security.

Use caution when your AI agent interacts with external services:

  • Store tokens securely (e.g., environment variables in secrets).
  • Review data exposures—especially in logs.
  • Rotate tokens and credentials regularly.

Defining and Automating Daily Developer TasksIdentifying Tasks and Designing Task WorkflowsList out routine tasks that could benefit from LLM-powered automation:

  • Code scanning (security, style checks)
  • Creating draft release notes
  • Debugging suggestions from logs
  • Summarizing PRs, especially large ones
  • Triage and labeling of new GitHub issues

Define the input and output requirements:

  • Clear triggers like pushing a commit or a tag
  • Well-defined outputs like AI-generated summaries, posted comments, or updated Slack messages

Define consistent error handling:

  • Retry with exponential back-off
  • Notify a Slack channel or email list for manual intervention

Implementation DetailsCode Structure:

  • Use a logical folder structure.
  • This makes it easier to add or refine tasks without confusion.

Logging and Data Storage:

  • Agents often produce valuable metadata such as logs of LLM calls, error messages, or final decisions.
  • Decide on a storage approach (like local JSON logs or a time-series database) that your team can later review or analyze for improvements.

Code Reviews and Commit SummariesIn this step, we’ll explore a small Node.js agent powered by the Vercel AI SDK that automatically reviews commits, summarizes code changes, and flags security or style issues. The goal is to reduce manual overhead and let developers focus on high-impact work rather than repetitive, preliminary checks.

Benefits and Key Objectives for Commit Agent Automated Commit Triggers + Integrate your AI agent with GitHub’s webhooks or rely on Semaphore’s pipeline triggers whenever a new commit is pushed. + This ensures immediate detection of code changes and helps your team fix problems at the earliest possible time. * Intelligent Summaries & Security Checks + By leveraging GPT-4 or other LLMs, your AI agent can parse diffs and highlight potential vulnerabilities, style problems, or major architectural changes. + Reviewing a large commit becomes easier when a summarized outline is readily available. * Actionable Feedback & Suggestions + Developers can receive direct improvement tips—from refactoring opportunities to adopting best practices for dependency management. + These suggestions reduce the mental overhead required during code review. * Seamless CI/CD Integration* + With the help of Semaphore blocks and jobs, you can automatically run the code review agent removing the need for manual triggers. + Comments or logs can be posted to PR threads enabling quick, iterative feedback loops.

Getting the ProjectTo access the code review agent, clone the repository:

git clone git@github.com:ajcwebdev/semaphore-ai-agents.gitcd semaphore-ai-agents Project DependenciesThe project uses the following key dependencies:

  • ai and @ai-sdk/openai – The core Vercel AI SDK and OpenAI provider packages
  • zod – Helps define schemas for validating or structuring data

Environment SetupThe project requires a .env file with the following variables:

OPENAI_API_KEY=YOUR_OPENAI_KEYGITHUB_TOKEN=YOUR_GITHUB_TOKENGITHUB_OWNER=YOUR_GITHUB_USERNAMEGITHUB_REPO=YOUR_GITHUB_REPO For example, if I want my AI Assistant to review this codebase ajcwebdev/semaphore-ai-agents, I’d set GITHUB_OWNER=ajcwebdev& GITHUB_REPO=semaphore-ai-agents. If you’ve forked the repo, you will have the same GITHUB_REPO name but your own GITHUB_OWNER account name.

You’ll need to create a fine-grained personal access token on GitHub at github.com/settings/personal-access-tokens. The only permission required is read-only access to the public GitHub repository that is connecting to Semaphore.

Understanding the Code StructureThe overall project structure looks like this:

semaphore-ai-agents/├── .env├── .gitignore├── package.json├── src/│ ├── config.ts│ ├── index.ts│ ├── ai/│ │ └── review.ts│ ├── commits/│ │ └── parser.ts│ └── github/│ └── tools.ts└── .semaphore/ └── semaphore.yml Let’s examine the key components of the commit analysis agent:

  1. Configuration Module (src/config.ts)This module manages environment variables and validation. The code loads environment variables and provides a validation function to ensure all required configuration is present before proceeding.

  2. GitHub API Tools (src/github/tools.ts)This module handles GitHub API interactions.

This module provides two key functions:

  • fetchCommitDiff – An AI SDK tool that retrieves the diff for a specific commit SHA
  • fetchCompareCommits – A function that fetches all commits between two SHAs using GitHub’s compare API

  • Commit Parser (src/commits/parser.ts)This module handles parsing commit ranges. The code intelligently resolves the commit ranges provided by Semaphore CI environment variables. It handles both:

  • Single commits (when only SEMAPHORE_GIT_SHA is available)

  • Commit ranges using GitHub’s compare API (when SEMAPHORE_GIT_COMMIT_RANGE is available)

It also converts two-dot notation (..) to three-dot notation (...) for compatibility with GitHub’s API.

  1. AI Review Generator (src/ai/review.ts)This module generates the AI-powered code reviews. The module uses the Vercel AI SDK to generate intelligent code reviews. It:

  2. Uses GPT-4o to analyze commit diffs

  3. Provides specific instructions to summarize changes, flag issues, and suggest improvements
  4. Uses the fetchCommitDiff tool to retrieve the code changes for analysis
  5. Limits execution to 2 steps to optimize performance

  6. Main Entry Point (src/index.ts)This is the main orchestration file that brings everything together. The main file coordinates the entire review process:

  7. Validates the environment configuration

  8. Resolves the commits to review
  9. Generates AI reviews for each commit
  10. Outputs the reviews to the console
  11. Handles errors properly to ensure CI failures are reported

Semaphore CI/CD IntegrationOpen the Settings page on your Semaphore account and select the “Secrets” tab to create pr-review-secrets. Include the same environment variables inside your project’s .env file.

Semaphore Configuration FileThe project includes a Semaphore configuration file to trigger the agent on new commits. The AI output is captured in a file called ai-review.log and stored as an artifact for easy access in the Semaphore UI.

```

.semaphore/semaphore.ymlversion: v1.0name: Code Review Pipelineagent: machine: type: e1-standard-2 os_image: ubuntu2004global_job_config: prologue: commands: - checkout --depth=full - npm installblocks: - name: Commit Review task: secrets: # Create pr-review-secrets in the Semaphore UI or with the Semaphore CLI - name: pr-review-secrets jobs: - name: Review Commits commands: # Run AI review script, capturing the output in a log file - npx tsx src/index.ts | tee ai-review.log # Push AI review log as a job artifact, view from Semaphore UI Artifacts tab. - artifact push job ai-review.log dependencies: []

``` This configuration:

  • Sets up a build pipeline that runs on Ubuntu 20.04
  • Checks out the full commit history to ensure proper diff generation
  • Installs dependencies and runs the review script
  • Saves the AI review output to ai-review.log, then pushes it as a job artifact
  • Uses a Semaphore secret named pr-review-secrets to securely provide the required environment variables

When Semaphore triggers a build on new commits, it will set SEMAPHORE_GIT_COMMIT_RANGE (or SEMAPHORE_GIT_SHA if only one commit is pushed). The script retrieves each commit’s diff and generates an AI review.

The review is stored both in the Semaphore logs (via tee) and as an artifact (ai-review.log). You can open the job log to read the review directly:

Or go to the Artifacts tab in your Semaphore job page to download the file:

Benefits of This Modular ApproachThe repository’s modular structure offers several advantages:

  1. Separation of concerns: Each file handles a specific aspect of the application
  2. Testability: Components can be tested in isolation
  3. Maintainability: Smaller focused files are easier to understand and modify
  4. Reusability: The modules can be reused across different parts of the application

By leveraging this AI-powered commit review agent, your team can gain immediate insights about each commit in a push or pull request, helping to identify potential issues before they reach human reviewers. Storing the artifact with the AI’s output makes it easy for the whole team to access and reference code changes.

ConclusionRecapWe started by discussing the motivations behind AI agents and why they can surpass traditional, rule-based automation. We walked through the basics of setting up your project, selecting the right frameworks, and defining your automated workflows. Then we dove into how to build an AI agent for commit analysis that provides summaries, security checks, and style feedback.

Best Practices Prompt Engineering: Craft system messages and prompts that guide the LLM to produce actionable feedback. * Secure Credential Management: Always store tokens and secrets in a protected environment, such as Semaphore’s secrets. * Monitoring & Iteration: AI agents improve with continuous prompts, data logs, and examples. Regularly review their suggestions for accuracy. * Limiting Permissions*: Provide the minimal necessary access—reduce the risk of unintentional modifications.

Next Steps & Future Improvements1. Advanced Analytics: Integrate more robust usage metrics and dashboards to refine the agent’s suggestions over time. 2. Complex Integrations: Explore adding JIRA or Asana tasks automatically based on your agent’s analysis or code updates. 3. Refined Prompting Strategies: Use conversation memory or chain-of-thought techniques for even more accurate agent responses. 4. Support for Additional Developer Tools: Incorporate tools like ESLint, SonarQube, or custom security scanners for deeper code insights.

Call to ActionClone or fork the example repository, add your own environment variables, and integrate the AI agent with your existing CI/CD pipeline on Semaphore. You’ll quickly see how these agents can reduce your overhead while improving code quality and release workflows.

Resources* Semaphore Documentation * Vercel AI SDK and OpenAI Provider * LangChain * GitHub Actions for LLM-based PR review * AgentGPT and AutoGPT

The post How to Build an AI Agent to Help with Daily Tasks appeared first on Semaphore.

View Details

Ever hit ‘run’ on your CI/CD pipeline, only to grab a coffee… and it’s still not done when you get back? We’ve all been there. Slow builds can feel like a productivity killer — but there’s a better way.

Semaphore’s self-hosted agents give you control — delivering faster builds, improved scalability, and enhanced security by running agents within your infrastructure. Whether you’re scaling build pipelines for a growing team or need flexible deployment options, self-hosted agents can solve your CI/CD headaches.

Let’s dive into how they work — and how you can get the most out of them.

To learn more about self hosted agents, you can listen the podcast or watch the video below:

What Are Semaphore’s Self-Hosted Agents?Semaphore’s architecture is built for performance and flexibility, consisting of two key parts — the brains and the brawn:

  • Control Plane: The brains behind Semaphore that orchestrates pipelines, manages secrets, and controls job execution.
  • Agent Pool: The brawn — a set of workers that execute your build and deployment jobs. These can run on Semaphore’s infrastructure or as self-hosted agents within your environment.

How to Register a Self-Hosted Agent in Semaphore (Step-by-Step Guide)When you spin up a self-hosted agent, the first thing it does is knock on the control plane’s door, asking for permission to join the party.

Here’s what happens next:

  • The agent sends a registration request to the control plane. All agents of the same type use the same registration token.
  • The control plane assigns the agent a unique access token for secure communication.

Semaphore enforces quotas to ensure your infrastructure doesn’t accidentally spawn an army of rogue agents. Each organization has a maximum number of agents allowed to run at once. To ensure this is managed properly across multiple self-hosted hub instances, Semaphore uses Postgres advisory locks to guarantee consistency — no unexpected agent overloads.

Mastering the Semaphore Agent Lifecycle for Reliable BuildsOnce registered, an agent moves through predictable states.

  • Waiting for Job: The agent stays idle until assigned a job.
  • Running Job: The agent executes the assigned job.
  • Finished Job: The agent either disconnects or returns to the waiting state based on your configuration.

Semaphore’s sync protocol efficiently manages state transitions, ensuring agents respond quickly while avoiding excessive system load.

How Semaphore Controls CI/CD Logs for Maximum EfficiencyLogs are a lifeline when debugging. Semaphore’s Log Hub service makes sure they stay organized and efficient:

  • Receives log batches from agents.
  • Stores active logs in Redis for fast access.
  • Archives completed job logs in object storage (e.g., S3) for long-term retention.

But what if your build spits out a massive wall of logs? Semaphore caps log uploads to prevent system overload. Don’t worry — the agent will still keep those logs locally, so you can upload them later as artifacts for detailed debugging.

Scaling Semaphore Agents for Fast, Reliable DeploymentsScaling agents can be tricky — unless you let Semaphore do the heavy lifting. Whether you’re running AWS, Kubernetes, or your own setup, Semaphore’s tools help you scale smart, without extra work:

  • On AWS: Tools like AWS CDK automate the deployment of auto-scaling groups, manage AMIs for consistent environments, and configure IAM roles securely.
  • On Kubernetes: Custom controllers manage agent scaling dynamically.

Semaphore’s dedicated tools simplify deployment and ensure your agents scale as your workloads grow.

Key Takeaways for Developers Faster Builds, Less Waiting: Self-hosted agents eliminate bottlenecks, improving your CI/CD performance. * Flexible Deployment Options: Deploy agents on AWS, Kubernetes, or your own infrastructure to suit your needs. * Reliable Scaling:* Semaphore’s smart architecture handles dynamic scaling without manual intervention.

Try Semaphore’s Self-Hosted Agents TodaySemaphore continues to evolve with support from the open-source community, making it easier than ever to build and deploy quality software at scale.

🚀 Ready to stop waiting on slow builds and ship faster? Get Started with Semaphore’s Self-Hosted Agents Now.

Have questions or insights on self-hosted agents? Join the conversation in our community!

The post How to Scale CI/CD With Self-Hosted Agents appeared first on Semaphore.

View Details

Acceptance testing is essential for verifying that software meets user requirements by ensuring it functions as expected. It validates the system’s behavior against predefined criteria, providing confidence in the final product. However, traditional acceptance testing methods often struggle with communication gaps between technical teams and non-technical stakeholders, leading to costly rework and project delays.

A common scenario highlights this issue: a business analyst (BA) gathers requirements from a client and then communicates them to the development team. Despite best efforts, different interpretations can cause discrepancies between what the client envisions and what the developer delivers. These misunderstandings in requirements are a major challenge for successful software delivery.

By improving collaboration through methods like Behavior-Driven Development (BDD), teams can bridge the gap between stakeholders, minimize misunderstandings, and ensure alignment from the start.

Behavior-Driven Development (BDD) is a software development methodology that bridges the gap between business requirements and technical implementation. It encourages collaboration among developers, testers, and business stakeholders. They define together how the system should behave in a shared language.

In this article, we’ll explore how to combine BDD with acceptance testing. You’ll see how BDD transforms acceptance tests into living documentation. We’ll also discuss how to automate these tests using Semaphore CI. By the end of this article, you’ll understand why BDD is the best approach for creating effective and maintainable acceptance tests.

What is Acceptance Testing?Acceptance testing ensures a system meets user requirements and behaves as expected in real-world scenarios. It confirms the software delivers the functionality stakeholders agreed upon. It is a crucial step and can either be the final phase or come just before end-to-end testing.

Traditionally, acceptance testing was performed manually. Business analysts or QA teams would write test cases based on requirements and execute them step-by-step to confirm expected behavior. While manual testing works for small projects, it becomes time-consuming, error-prone, and unsustainable for modern software development.

Today acceptance testing relies on automation to overcome these challenges. By automating acceptance tests, teams can validate behavior continuously throughout the development lifecycle. This not only reduces manual effort but also ensures that changes do not break existing functionality.

Here’s an example of an acceptance test:

A requirement states, “The system must allow a user to reset their password.” A simple acceptance test would verify this behavior:

  1. The user navigates to the “Forgot Password” page.
  2. The user submits their email.
  3. The system sends a reset link to the user’s email.
  4. The user resets the password successfully.

The goal of acceptance testing is to confirm that the system satisfies these steps from the user’s perspective.

However, manually defining and maintaining such test scenarios often leads to inconsistencies and misunderstandings. This is where BDD comes into play. By structuring acceptance tests as executable specifications, BDD eliminates ambiguity and improves team collaboration.

In the next section, we’ll explore how BDD works and why it’s a good idea to combine it with acceptance testing.

How BDD WorksNow, let’s dive deeper into how BDD works and why it’s transformative for acceptance testing.

Structure of BDDThe core idea of BDD is to write scenarios that define how the system should behave. These scenarios use natural, human-readable language that both technical and non-technical team members can understand. A fundamental aspect of BDD is the Gherkin syntax, which structures acceptance criteria into a readable format. It uses the Given-When-Then pattern. The tests express the user’s point of view.

  • Given: Describes the preconditions or context.
  • When: Specifies the action or event.
  • Then: States the expected outcome.

For example, let’s revisit the password reset requirement:

Feature: Password Reset Scenario: Successful password reset request Given the user is on the "Forgot Password" page When the user submits their valid email address "user@example.com" Then the system sends a password reset link to "user@example.com" You can also enhance the clarity and expressiveness of scenarios by using additional keywords like And, But, and Examples.

  • And: This keyword allows you to chain multiple actions or expectations in a natural way. For example, “Given the user is logged in, and the user has admin privileges.”
  • But: This is a way to introduce an exception or a contrasting condition. For example, “When the user clicks the button, but the internet connection is lost.”
  • Examples: The Examples keyword is useful for data-driven testing. It allows you to run the same scenario with multiple data. For example, “Given I am on the login page when I enter the following username and password: Examples Username | Password | Expected Result.”

This is how BDD transforms business requirements into executable scenarios that serve as documentation and tests.

The Benefits of BDDBDD brings several advantages to software development:

  • Shared Understanding: Scenarios are written in plain language, enabling collaboration between stakeholders, developers, and testers.
  • Executable Specifications: Scenarios are not just text—they can be automated and executed to validate behavior.
  • Reduced misunderstandings: By involving stakeholders in defining behaviors, BDD minimizes miscommunication and ensures clarity in requirements.
  • Lower costs: Preventing misunderstandings and errors early in the development cycle saves time and resources.
  • Safer releases: Scenarios act as living documentation, ensuring new changes don’t disrupt existing functionality.

When to Use BDD?BDD scenarios are not necessarily mandatory for every product specification. They are most useful for complex specifications where there is a risk of misunderstanding or when more thorough testing is needed. For simple tasks, like color or text changes, using BDD scenarios would be inefficient and unnecessary.

BDD and User StoriesIn practice, teams can effectively integrate BDD with user stories in Jira. For each user story, they create a corresponding BDD scenario, often in the Given-When-Then format, which directly translates the acceptance criteria into automated tests.

Jira User Story:

As a user, I want to be able to reset my password using my email address so that I can regain access to my account if I forget my password.

Acceptance Criteria (in the user story):

The user should be able to request a password reset by entering their registered email. The user should receive a password reset email with a link to reset their password. The link should expire after 30 minutes.

Corresponding BDD Scenario:

Feature: Password ResetScenario: User requests a password resetGiven the user is on the login pageWhen the user enters their registered email and clicks "Reset Password"Then the user should receive a password reset emailAnd the email should contain a link to reset the passwordAnd the reset link should expire in 30 minutes BDD ChallengesWhile BDD offers significant benefits, teams often face challenges when implementing it effectively.

  1. Maintaining Tests
  2. Challenge: Keeping tests updated as the system evolves.
  3. Why it Happens: Poorly written tests may become outdated or too complex.
  4. Solution:
    • Balance detail and simplicity.
    • Regularly review and refactor tests.
    • Focus on aligning tests with evolving requirements.
  5. Team Collaboration
  6. Challenge: BDD relies heavily on collaboration between developers, testers, and stakeholders.
  7. Why it Matters : Without strong teamwork, tests may not accurately reflect the system’s intended behavior.
  8. Solution:
    • Foster open communication in Agile environments.
    • Build cross-functional teams with shared goals and understanding.
  9. Adopting the BDD Mindset
  10. Challenge: Shifting to BDD can feel daunting, especially for teams used to traditional testing methods.
  11. Why it Happens: BDD has a steep learning curve, leading to resistance.
  12. Solution:
    • Emphasize benefits like improved collaboration and alignment with business goals.
    • Share success stories and celebrate small wins.
  13. BDD Anti-Patterns
  14. Feature-Coupled Step Definitions: These occur when step definitions are tightly coupled to specific scenarios or features, making them hard to reuse. For example:

Given the user is logged inWhen the user clicks on "Add to Cart"Then the item is added to their shopping cart If this step relates to a single feature like “Add to Cart,” it might not be reusable for similar actions elsewhere, such as “Add to Wishlist.”

  • Solution:
    • Use generic, reusable steps:

When the user performs an action on "Add to Cart"Then the system updates the corresponding data * Conjunction Steps: These steps combine multiple actions into one, making tests harder to understand and debug. For instance:

When the user fills out the form and clicks submit It’s hard to read and unclear whether the issue lies with filling out the form or with clicking submit.

Solution: Break into smaller, clear steps:

When the user fills out the formAnd the user clicks submit 5. Trivial Functionality Tests: These are scenarios that test functionality already covered by unit tests, adding unnecessary complexity. * Why it Happens: Some teams might feel pressure to achieve high test coverage and include trivial cases in scenarios. Others may treat BDD as a replacement for all types of testing. * Solution: + Keep scenarios focused on what the system does, not how it works internally. + Use BDD to describe high-level behaviors or system-level interactions that matter to stakeholders.

Popular BDD ToolsSeveral tools support BDD by allowing teams to write and execute tests based on behavioral specifications:

  • Cucumber: Supports various programming languages and offers strong Gherkin support, making it highly popular. It can even sync with Jira to automatically link the scenarios to the relevant user stories.
  • Behave: A Python-based BDD framework ideal for teams working with Python projects.
  • Reqnroll: A popular BDD framework for .NET developers.

The Links Between BDD and Acceptance TestingExecutable Specifications

BDD turns acceptance tests into executable specifications, aligning technical implementation with business goals. Using Gherkin, teams write testable scenarios that serve as a single source of truth for all stakeholders. Tip: Structure scenarios with the Given-When-Then format for clarity and consistency.

Behavior as Acceptance Criteria

BDD emphasizes translating vague requirements into clear, testable scenarios. For example:

| Traditional Requirements | BDD Acceptance Criteria | | --- | --- | | “Allow password reset” | Scenario: Password Reset Request |

Collaboration Across Teams

BDD fosters a shared understanding between developers, testers, and stakeholders. Its simple Given/When/Then syntax allows everyone to contribute to defining behaviors.

Linking BDD to acceptance testing enhances clarity, collaboration, and confidence in meeting customer expectations.

Automating BDD Acceptance Tests with CI/CDIntegrating BDD tests into your CI/CD pipeline ensures continuous validation of features. Automated tests catch regressions early, meeting user expectations before deployment.

Here’s a simplified CI/CD pipeline flow when using BDD-based acceptance tests:

With tools like Semaphore CI, you can configure this workflow to run on every commit, branch, or merge request.

Automating Acceptance Tests with Semaphore CIIn this example, we’ll write a BDD test for a Python project using the Behave framework. We’ll automate the BDD tests using Semaphore CI’s pipeline.

First, create a virtual environment to safely install Python packages:

python3 -m venv myenvsource myenv/bin/activate Install Behave:

pip3 install behave Create a file named password_reset.feature in a features directory. This file contains the BDD scenarios using Gherkin syntax.

Feature: Password Reset In order to regain access to my account As a registered user I want to be able to reset my password via email Scenario: User submits a valid email for password reset Given the user is on the "Forgot Password" page When the user submits a valid email "user@example.com" Then the system sends a password reset link to "user@example.com" Scenario: User submits an invalid email for password reset Given the user is on the "Forgot Password" page When the user submits an invalid email "invalid_email" Then the system shows an error message "Please enter a valid email address" Create a file named password_reset_system.py to implement the system functionality:

class PasswordResetSystem: def __init__(self): self.valid_emails = [] # Store valid emails for testing purposes def add_valid_email(self, email): """Method to add valid email addresses.""" if self.is_valid_email(email): self.valid_emails.append(email) else: raise ValueError(f"Invalid email format: {email}") def reset_password(self, email): """Simulate the password reset request.""" if self.is_valid_email(email): return f"Password reset link sent to {email}" else: return "Please enter a valid email address" def is_valid_email(self, email): """Helper method to check if email is in valid format.""" return "@" in email and "." in email # Very basic email validation (see https://pypi.org/project/email-validator/) Create a main.py file where you initialize the PasswordResetSystem class and interact with it.

from password_reset_system import PasswordResetSystemdef main(): # Create an instance of the PasswordResetSystem system = PasswordResetSystem() # Example of adding a valid email try: system.add_valid_email("user@example.com") print("Valid email added!") except ValueError as e: print(e) # Simulate a password reset request with a valid email response = system.reset_password("user@example.com") print(response) # Output: Password reset link sent to user@example.com # Simulate a password reset request with an invalid email response = system.reset_password("invalid_email") print(response) # Output: Please enter a valid email addressif __name__ == "__main__": main() When you run main.py, you will see output similar to this:

$ python3 main.py Valid email added!Password reset link sent to user@example.comPlease enter a valid email address Now, create a steps.py file in a steps directory to define the steps in your scenarios. Behave will look for step definitions that match the steps in the feature file.

from behave import given, when, thenfrom password_reset_system import PasswordResetSystem# Initialize the system instancesystem = PasswordResetSystem()@given('the user is on the "Forgot Password" page')def step_given_user_on_forgot_password_page(context): context.page = "Forgot Password" context.system = system # Assign the system object to context for use in other steps@when('the user submits a valid email "{email}"')def step_when_user_submits_valid_email(context, email): context.response = context.system.reset_password(email)@when('the user submits an invalid email "{email}"')def step_when_user_submits_invalid_email(context, email): context.response = context.system.reset_password(email)@then('the system sends a password reset link to "{email}"')def step_then_system_sends_reset_link(context, email): assert context.response == f"Password reset link sent to {email}", f"Expected reset link for {email}, got {context.response}"@then('the system shows an error message "{message}"')def step_then_system_shows_error_message(context, message): assert context.response == message, f"Expected error message '{message}', got {context.response}" Key points:

  • This file defines the Python functions that implement the actions from the Gherkin steps. For example, when the user submits a valid or invalid email, the PasswordResetSystem class simulates the system’s behavior and returns the appropriate response.
  • We use assert to perform assertions. For example, we assert that the response from the reset_password method matches the expected output.

To run the tests, execute this command:

$ python -m behave You should see output similar to this:

$ python -m behaveFeature: Password Reset # features/password_reset.feature:1 In order to regain access to my account As a registered user I want to be able to reset my password via email Scenario: User submits a valid email for password reset # features/password_reset.feature:7 Given the user is on the "Forgot Password" page # steps/steps.py:8 0.000s When the user submits a valid email "user@example.com" # steps/steps.py:14 0.000s Then the system sends a password reset link to "user@example.com" # steps/steps.py:22 0.000s Scenario: User submits an invalid email for password reset # features/password_reset.feature:12 Given the user is on the "Forgot Password" page # steps/steps.py:8 0.000s When the user submits an invalid email "invalid_email" # steps/steps.py:18 0.000s Then the system shows an error message "Please enter a valid email address" # steps/steps.py:26 0.000s1 feature passed, 0 failed, 0 skipped2 scenarios passed, 0 failed, 0 skipped6 steps passed, 0 failed, 0 skipped, 0 undefinedTook 0m0.000s Generate a requirements.txt file, which includes all the Python dependencies you’ve installed in your environment:

$ pip freeze > requirements.txt Run the deactivate command to deactivate the virtual environment when you’re done.

Let’s move on to the CI part.

Semaphore expects a .semaphore folder at the root of your project, as well as a semaphore.yml file containing the CI configuration for the project:

version: v1.0name: Pythonagent: machine: type: e2-standard-2 os_image: ubuntu2204blocks: - name: "Install Dependencies" task: prologue: commands: - sudo apt-get update && sudo apt-get install -y python3-dev jobs: - name: pip commands: - checkout - cache restore - pip download --cache-dir .pip_cache -r requirements.txt - cache store - name: "Run Tests" task: prologue: commands: - checkout - cache restore - pip install -r requirements.txt --cache-dir .pip_cache jobs: - name: Run BDD Tests commands: - python -m behave Key points:

  • The e2-standard-2 machine type has 2 CPUs and 4GB of RAM, suitable for standard workloads.
  • The commands in the prologue execute before the block’s jobs run.
  • The “Install dependencies” block updates the system and installs Python dev tools. It also caches Python dependencies to speed up future runs.
  • The “Run Tests” block restores dependencies from the cache and installs them. Then it runs the BDD tests using python -m behave.

To execute the tests in the pipeline, you need a Semaphore account. Visit the signup page and choose either GitHub or Bitbucket for registration. In this example, I’ll use GitHub.

Next, create a new project by clicking the “+ Create new” button.

Add a GitHub repository. For example, I’ll use the demo project I created for this tutorial.

Semaphore will automatically initialize the project. As you already have the semaphore.yml file in place, you can use it directly in the pipeline workflow. Each push to the project triggers the pipeline.

You can expect results like this:

The “Run BDD Tests” job displays the test results. You should see the same output as you did earlier in your Terminal:

You can find the complete source code for this demo in my GitHub repository.

ConclusionIn this article, we examined BDD’s functionality, benefits, challenges, and its integration with acceptance testing to enhance collaboration and develop effective testing strategies. Utilizing BDD with acceptance testing boosts team confidence by ensuring features address user needs.

You also learned how to implement a BDD test and automate it within the Semaphore CI pipeline.

Start small—identify critical business scenarios, write them in the Given-When-Then format, and automate them as acceptance tests. Once you integrate them into your CI/CD pipeline, these tests will ensure quick feedback and more reliable software delivery.

The post Accelerate your CI/CD Pipeline with BDD and Acceptance Testing appeared first on Semaphore.

View Details

I’m thrilled to share transformative news about Semaphore’s evolution. After serving developers through our commercial offering for over 12 years, we’re taking a significant step by open sourcing our core platform under the Apache 2.0 license. This decision reflects our belief that the most powerful solutions emerge when developers can deeply understand, modify, and enhance their tools.

Why the changeWhen we started in 2012, commercial, closed-source platforms like GitHub and Heroku showed the way forward in cloud services. Times have changed dramatically since then: Docker and Kubernetes have standardized cloud and on-prem deployment, security has become crucial for all companies, and organizations like Gitlab and HashiCorp have proven sustainable open source business models.

Yet today’s CI/CD landscape presents developers with an uncomfortable choice: they can either use open source tools that offer flexibility but struggle with scalability and user experience, or accept vendor lock-in from platforms where CI/CD is just another checkbox feature. Neither path fully serves modern development teams’ needs.

Semaphore has grown beyond our initial vision, shaped by thousands of hours of collaboration with our amazing customers. Each feature and improvement in developer experience has emerged from deep partnerships with teams solving real-world delivery challenges. Opening our entire platform to developers worldwide is the natural next step – an opportunity to expand this collaborative spirit to a broader community.

The new modelOn February 26th, the core platform that powers Semaphore will be available on GitHub under Apache 2.0 license. This production-grade codebase, from our pipeline orchestration engine to our workflow builder, processes millions of workflows across thousands of organizations today.

Semaphore will be available in three editions to meet different needs:

  1. Semaphore Cloud remains our fully-managed service, offering both pure cloud and hybrid solutions for teams that want a seamless experience and the fastest CI/CD pipelines on the market, and access to all advanced features like pipeline promotions and flaky test detection.
  2. Semaphore Enterprise Edition provides on-premises deployment with advanced security and compliance features that larger organizations require.
  3. Semaphore Community Edition represents our core platform, now open source and free to use on your own infrastructure. This gives developers complete control to run and customize Semaphore for their specific needs.

Each edition maintains clear boundaries between open source and commercial features, ensuring a sustainable model where community innovation and enterprise reliability reinforce each other.

For our current usersWe have carefully designed this transition to ensure continuity for all our users. If you’re using Semaphore Cloud today, your experience stays the same – you’ll continue to enjoy our fully-managed service and industry-leading pipeline performance. Enterprise On-prem customers maintain access to all their current features, with the added benefit of being able to examine and extend the core platform code.

The open sourcing of our core platform creates new opportunities for all our users. You’ll be able to better understand how Semaphore works, contribute improvements that matter to your team, and build custom integrations with unprecedented flexibility.

Our commitment to security and reliability remains unchanged – the same components that have reliably processed millions of your workflows are the ones we’re opening to the community.

For developersThis isn’t another half-baked open source release. We’ve spent a decade building a developer-focused CI/CD platform that makes complex workflows clear and simple, at any scale. Semaphore’s code and infrastructure have passed a comprehensive security audit by Cure53, and we’re opening production-grade code that processes millions of jobs daily.

Our mission now is to build the next major milestones together with the community. We’re embracing full transparency by moving all our product and engineering discussions to public channels on Discord, GitHub, and YouTube. We believe this “building in public” approach, combined with our proven UX and scalability, creates the foundation for a new standard in CI/CD that developers have been waiting for.

The path forwardOver the next three weeks, we will be preparing for the open source release by:

  • Transitioning to our new home at semaphore.io
  • Publishing comprehensive documentation, including architecture diagrams and contribution guides
  • Preparing the main repository with issue templates and CI/CD pipelines

The journey culminates with Semaphore Summit, our online community event starting February 26. The Summit opens with a keynote announcing Semaphore’s open source release, where we’ll make the code publicly available and share our vision for the future of CI/CD.

Later that day, I’ll host a fireside chat to dive deeper into our technical roadmap and answer your questions about contributing to Semaphore.

The Summit continues over three days, with a few focused sessions each day to make it easy to attend alongside your regular work. You’ll hear from teams who’ve built remarkable delivery pipelines, join technical deep-dives into advanced patterns, and participate in hands-on contribution workshops. We’ve designed each day’s schedule to be both valuable and manageable, whether you join us for specific sessions or the entire event.

Get involvedJoin us at Semaphore Summit:

  • February 26-28, 2025
  • Online and free
  • Register here

This marks the beginning of a new chapter in CI/CD – one where developer experience, enterprise reliability, and community innovation come together. With production-grade foundations and a commitment to building in public, we’re creating the open platform we wish had existed when we started our journey. I’m excited to see what we’ll build together.

Darko,

Semaphore CTO and co-founder

The post Semaphore is Going Open Source in 30 days appeared first on Semaphore.

View Details

As we step into 2025, the tech landscape is set for another transformative year. From artificial intelligence (AI) making strides in automation and creativity to the looming potential of quantum computing, this year promises to be pivotal. Let’s explore the key developments and expected tech trends 2025.


AI: Growing Influence and ChallengesLegal Battles Over AI Training Data
AI continues to make headlines, but not without controversy. OpenAI recently won a round of lawsuits from news outlets over its use of trending data for training AI models. However, this doesn’t mean the fight is over—lawsuits like the one from The New York Times, alleging unauthorized use of journal articles, could set important precedents for the future of AI training. If these cases swing in favor of content creators, it could change how AI companies operate, particularly in how they source their data.

AI-Generated Code on the Rise
AI-powered tools are increasingly being used to write code. Google reported that 20% of code written in 2024 was AI-generated—a figure that’s expected to grow. Developers are divided on this, though. While many see AI as a helpful tool, around 40% of developers don’t fully trust the quality of AI-generated code. Concerns about technical debt and reliability may push companies to demand better-performing models this year.

Tech trends for 2025: AI Agents Gaining Momentum
AI agents—software designed to perform tasks on your behalf—are expected to be a big focus in 2025. Companies like Anthropic, OpenAI, Google, and Microsoft are all diving into this space. These agents can handle tasks ranging from personal assistance, like ordering food or booking trips, to business processes, such as automating workflows or writing code. While this technology is exciting, its adoption will depend heavily on how well it can be integrated into daily life.

Small AI Models and Edge Computing
Not all AI models are massive and resource-hungry. In 2025, smaller, more efficient AI models that can run directly on devices like phones or laptops will continue to emerge. These models make AI features feel more seamless and accessible, especially for specialized tasks. Open-source projects are leading this tech trend for 2025, allowing users to fine-tune models for their specific needs without relying heavily on cloud-based systems.


Tech Trends for 2025: The Rise of AI in Video and Creative Industries
AI’s ability to generate content is expanding from images to videos. OpenAI’s Sora and Google’s Veo2 models are already creating 20-second video clips from text prompts. While this technology is still in its early stages, it could disrupt industries like stock video, much like how AI reshaped stock photography. The tech trends for 2025 are likely to see even more advancements in text-to-video capabilities, making high-quality video production faster and more accessible.


Security and AI-Driven Scams
AI’s potential isn’t all positive. Its growing power has also led to more sophisticated scams and cyber threats. In one alarming case last year, a scammer used AI-generated deepfake video to impersonate a company executive and steal $25 million. As these technologies improve, spotting fake communications will become increasingly difficult. Businesses and individuals will need to be more vigilant to protect themselves from these advanced threats.


Tech trends for 2025: Sustainability Concerns and AI’s Energy Demands
The rapid growth of AI as a tech trend for 2025 comes with significant energy requirements. It’s estimated that by 2027, AI’s energy consumption could quadruple compared to 2024 levels. Big tech companies are already taking action to address this. For example, Microsoft is reactivating the Three Mile Island nuclear facility, while Google and Amazon are pursuing small nuclear reactors to power their AI systems.

This highlights a major sustainability challenge for the AI industry. As the demand for AI-powered solutions grows, companies will need to focus on eco-friendly innovations, such as energy-efficient data centers and green cloud providers.


Quantum Computing: The Next Frontier
While AI continues to dominate discussions, quantum computing is quietly advancing in ways that could be even more disruptive. In late 2024, Google unveiled Willow, a quantum chip capable of solving problems that would take supercomputers billions of years, setting the tone for tech trends for 2025.

The Encryption Threat
One major concern with quantum computing is its potential to break current encryption methods, which secure much of the internet’s data. This has led to the creation of initiatives like the Linux Foundation’s Post-Quantum Cryptography Alliance, which aims to develop quantum-resistant encryption algorithms.

Companies need to prepare now for the possibility of “Harvest Now, Decrypt Later” attacks, where encrypted data is stolen today and later decrypted using quantum technology. In 2025, expect to see more organizations adopting quantum-safe cryptography to protect sensitive information.


Tech trends for 2025: Looking AheadFrom the continued growth of AI in creative industries and everyday tasks to the challenges of sustainability and quantum security, 2025 is shaping up to be a year of both innovation and caution.

  • For businesses: Keep an eye on AI developments that could enhance efficiency, but stay aware of the legal and ethical implications.
  • For individuals: Be prepared for AI to become a bigger part of your life, both in convenience and in the need to spot potential scams.
  • For everyone: Quantum computing may feel far off, but its impacts could be closer than we think—especially when it comes to online security.

As we navigate this transformative year, staying informed and adaptable will be key to making the most of these exciting, but complex, technologies.

The post What to Expect: Tech Trends For 2025 appeared first on Semaphore.

View Details

Smoke and sanity testing are two of the most common software testing methodologies. While “sanity testing” is often mistakenly used as a synonym for “smoke testing,” the two focus on different aspects. Understanding their key differences is important to avoid confusion.

Smoke testing involves verifying that the key functionalities of an application still work after a new build. In contrast, sanity testing is more focused, checking that specific changes made to an already validated build work as expected.

In this smoke vs sanity article, you will learn what smoke and sanity testing are, how they work, when to use them, and whether they can be effectively combined.

Let’s dive in!

What Is Smoke Testing?Smoke testing, also called “confidence testing,” “build promotion testing,” or “build verification testing,” is a type of software testing aimed at varying that critical system functionalities work as expected after a new build.

It aims to identify major issues and determine whether the build is stable enough for further testing. As explained in Lessons Learned in Software Testing by Cem Kaner, James Bach, and Brett Pettichord: “The phrase smoke test comes from electronic hardware testing. You plug in a new board and turn on the power. If you see smoke coming from the board, turn off the power. You don’t have to do any more testing.”

The practice gained popularity with the advent of Agile development methodologies, where frequent builds are a priority. Smoke testing ensures stability early in the development lifecycle, helping teams decide whether to continue the deployment.

How to Apply Smoke TestingAt its core, smoke testing follows a straightforward process:

Everything starts with the development of a new feature, update, or an entirely new version of the software. Once the development is complete, a new software build is produced and typically sent to a CI/CD pipeline for deployment.

Next, smoke tests are executed. These tests are a selected subset of test cases—including unit tests, E2E tests, API tests, and more. Their goal is to test the most important functionality of the system, ensuring that core features are still working as intended.

If the smoke tests pass, the deployment process continues with more specific testing phases, such as regression or integration tests. On the contrary, if the smoke tests fail, the deployment is halted. The build is rejected and sent back to the development team to address the identified issues. Once fixed, a new build is created, and the cycle repeats until the software passes the smoke testing phase.

In other words, smoke testing is a critical checkpoint in software development. It acts as the first line of defense to ensure that new builds are stable and ready for deployment. This is particularly effective in an automated testing pipeline, as it helps teams quickly understand whether to proceed with deployment or stop it immediately, saving time and resources.

Example of a Smoke Test ScenarioImagine an e-commerce platform where a new payment gateway integration has been added to the checkout process. The development team has modified the application to include that new gateway, which allows customers to choose between multiple payment methods.

The key functionalities that should be verified, regardless of the changes made in an e-commerce application, include:

  • Login and sign-up workflow: Ensure that users are still able to log in and create new accounts.
  • Cart and checkout workflow: Users still need to be able to add items to the cart, proceed to checkout, and fill in necessary details such as shipping address.
  • Payment methods: Check that all payment options are available and function correctly.
  • Order confirmation: Ensure that after payment—regardless of the chosen payment method—the order is processed and the user receives a confirmation of the successful purchase.

These smoke tests confirm whether the e-commerce application still works correctly after the new build.

Smoke Testing Frameworks and ToolsThere are no dedicated tools or frameworks designed specifically for smoke testing. Since every software application is unique, the choice of tools depends on the specific functionalities you need to verify in your smoke tests.

For example, in one application, the key functionalities might involve API endpoints, while in another, it could focus on ensuring that the login workflow functions correctly. For testing API endpoints, tools like Postman or Swagger are highly effective. If your smoke tests target core business logic, unit testing frameworks such as JUnit, Jest, or Mocha are well-suited. On the other hand, when smoke testing involves verifying complete user workflows, tools like Playwright, Cypress, or Selenium are ideal choices.

Ultimately, smoke testing is less about the tools themselves and more about the strategy—selecting the appropriate tests to efficiently verify the build’s stability.

Pros and Cons of Smoke TestingLet’s discover the main advantages and disadvantages of smoke testing!

👍 Pros:

  • Early detection of critical issues
  • Can be run frequently and quickly, as it generally involves only a limited subset of test cases
  • Improves build quality
  • Can be integrated into CI/CD pipelines for testing automation
  • Enhances team productivity by saving time thanks to its shift-left approach to testing

👎 Cons:

  • Identifying the right smoke tests requires skills and can be time-consuming, especially in larger projects
  • Passing smoke tests can create an illusion of stability, as deeper issues may exist in the application
  • Requires a lot of maintenance as the smoke testing suite needs regular updates to reflect changes in core functionality

Wonderful! Time to continue this smoke testing vs sanity testing comparison by exploring sanity testing.

What Is Sanity Testing?Sanity testing, also known as “surface level testing,” is a type of software testing aimed at verifying that specific functionalities or bug fixes in a software build are working as expected. It focuses on checking that minor changes to the application have not introduced unintended issues or caused core features to break.

Sanity testing is a narrow and quick check to confirm that the software is stable enough for more comprehensive testing. In most cases, it is performed after smoke testing.

How to Apply Sanity TestingThe cycle of sanity testing is similar to that of smoke testing. The main difference is that it starts from a more stable build that has already passed smoke testing, rather than from a new build that has yet to be tested.

The main goal of this testing process is to validate changes made to one or a few specific parts of the application. It is a targeted approach that focuses on specific components to ensure they are functioning properly after changes.

Sanity tests perform a quick and superficial check to confirm that the system still works as expected after a modification. Due to its focused and narrow scope, sanity testing tends to be carried out manually by software engineers or QA experts and is often not automated.

Example of a Sanity Test ScenarioConsider again the example of an e-commerce application where the development team has introduced a new payment gateway. After the application has passed smoke testing, sanity tests would focus solely on the feature that has changed—the payment system.

Testers will ensure that all payment methods work as intended, thoroughly validating these features to confirm that the new payment gateway integration functions correctly without affecting other parts of the application.

Sanity Testing Frameworks and ToolsIn most cases, sanity testing does not require dedicated frameworks, as it is achieved manually and without scripts. However, in cases where a more robust and/or automated sanity testing process is needed, the same considerations applied to smoke tests also apply to sanity tests.

Pros and Cons of Sanity TestingLet’s see the main benefits and drawbacks of sanity testing!

👍 Pros:

  • Cost-effective, as it is simple and not structured
  • Narrow and quick, with focused verification on areas of the application that have changed
  • Increases confidence in the robustness of a given build

👎 Cons:

  • May require manual effort, as it may not be easily automated
  • Effectiveness depends on the tester’s ability (at this exact moment) to accurately identify issues

Can Smoke Tests and Sanity Tests Coexist?TL;DR: Yes, smoke and sanity tests can be used together.

Sanity and smoke tests can coexist in a software development cycle, as they serve distinct but complementary purposes:

Smoke testing is the first step, verifying that core functionalities are working after a new build. Once smoke testing is successful, builds become more stable, and sanity testing is carried out. That focuses on specific changes to ensure those updates did not introduce new issues and bugs.

While both testing approaches can be used together, they are typically applied in different scenarios. Smoke testing is often automated and integrated into CI/CD pipelines, while sanity testing is generally done manually. In a typical process, smoke tests can be automated, and once they pass, manual sanity tests are performed on the changed features to confirm they work as expected.

If you prefer a fully automated approach, you may rely solely on smoke testing or invest in creating automated sanity tests. At the same time, automating sanity tests can be time-consuming and may not justify the effort, as these tests are extremely specific and hard to re-use.

Smoke vs Sanity Testing: Summary TableExplore the differences between the two approaches to testing in the smoke vs sanity comparison table below:

| Aspect | Smoke Testing | Sanity Testing | | --- | --- | --- | | Purpose | Verify that the core functionalities of an application are still working | Check that bugs have been resolved and the introduced changes are functioning as expected | | Goal | Ensures the build is stable enough for further testing | Ensures an application works after changes or fixes to proceed or not with further testing | | When performed | On a new build | On a new already validated build, usually after smoke testing | | Test Execution | Automated or manually | Typically performed manually | | Executed through | A subset of automated test scripts | Usually, manually by testers, developers, or QA experts | | Coverage | Covers core functionalities | Focuses on specific modules where changes were made | | Type of tests | Unit, E2E, integration, etc. (based on the key features of an application) | Depends on the changes to test | | Frameworks and tools | CI/CD tools (Jenkins, CircleCI, Semaphore), API testing (Postman, REST Assured), UI testing (Selenium, Cypress, Playwright), unit testing (JUnit, Jest), custom test automation frameworks, and more | Manual testing tools (TestRail, Zephyr), bug tracking (Jira, Bugzilla), exploratory testing tools, development IDEs for quick verification, and more |

ConclusionIn this smoke vs sanity testing comparison guide, you learned about the differences between the two. While they are often confused and sanity testing is sometimes seen as the same as smoke testing, they are actually distinct methodologies. Both share a similar testing loop, but they are applied at different stages of the development lifecycle.

The post Smoke Testing vs. Sanity Testing appeared first on Semaphore.

View Details

The Security and Compliance program at Semaphore just hit a major milestone – we’ve officially received our SOC 2 Type 2 certification. This means your code and data are even safer with us than ever before. (Not that they weren’t safe before – we’re just proving it now! 🙂)

What This MeansSOC 2 Type 2 compliance means we’ve undergone a thorough, independent audit of our security practices over an extended period. Unlike a point-in-time assessment, this certification verifies that we consistently maintain robust security practices in our day-to-day operations.

The Security You NeedThe certification process examined crucial aspects of our platform:

  • Security infrastructure and data protection
  • Access controls and authentication systems
  • Continuous monitoring and incident response
  • System availability and reliability
  • Disaster recovery and business continuity

For our customers, this certification provides concrete validation that your code, data, and development processes are protected by proven security measures that meet industry standards.

Why It MattersIf you’re using Semaphore (or considering us), this certification offers several key benefits:

  • Simplified vendor security assessments
  • Confidence in our security practices and controls
  • Independent verification of our operational excellence
  • Peace of mind for your security and compliance teams

Our Ongoing CommitmentWhile achieving SOC 2 Type 2 compliance is a significant milestone, we view it as just one step in our ongoing security journey. We remain committed to continuously improving our security posture and maintaining the trust you place in us.

Learn MoreFor those interested in the technical details or requiring our SOC 2 report for compliance purposes, please contact support@semaphore.com. We’re happy to provide more information under NDA.

Thank you to our customers for your trust, and to our team for their dedication to making this achievement possible.

The post Semaphore is Now SOC 2 Type 2 Compliant! appeared first on Semaphore.

View Details

Cloud adoption has outpaced the development of corresponding expertise in cloud operations, leading to a significant skills gap across the industry. In this regard, many organizations have had to abandon cloud initiatives because they lacked the necessary in-house expertise to manage them. As a result, critical projects are delayed, and costs and operational risks increase. Recognizing this gap, Cory O’Daniel co-founded MassDriver, a platform designed to simplify cloud infrastructure management for engineers without extensive cloud or operations experience. In this episode, Cory will explain how MassDriver allows teams to focus on building business value rather than getting bogged down in intricate cloud configurations.

Edited transcriptionAfter working at small and large software development companies, Cory O’Daniel found a widespread lack of knowledge about cloud infrastructure. Looking to fill this gap, he co-founded MassDriver, a platform as a service (PaaS) for cloud DevOps.

As Cory explains, MassDriver came to be from his effort to “make it easier for engineers that may not have this traditional cloud or operations experience to manage the cloud without getting inundated in the eight pages of docs about how to operate RDS, […] how to write Terraform, and how to build your CI pipelines.” In this way, the platform aims to relieve engineers from the burden of setting up their development environments and let them focus on building business value.

What MassDriver doesAccording to Cory, all software companies, whether they realize it or not, have a platform team. Even in cases where the platform is outsourced, he observes that companies are increasingly viewing the platform as an internal product, rather than “something this other team is doing in service for you.” The key distinction, Cory understands, lies in whether they are “consciously building a platform or not.” Considering this platform as the foundation for their development and deployment, Cory understands conscious platform building begins “as soon as you start getting to the cloud, starting to think, ‘how can I make these reproducible, reusable components that my software sits on top of so it makes it easy for me to make new environments or change things out?’

Targeting a user base of developers with no operational experience, MassDriver offers a creative system for building and managing complex cloud platforms. Under the hood, MassDriver follows the practice of infrastructure as code (IaC) for managing and provisioning infrastructure by describing it through code. However, MassDriver’s interface allows users to set up infrastructure using diagrams. As Cory explains, “There’s a number of infrastructure-as-code modules that are published into the platform; You can publish your own as well.” Companies can diagram the infrastructure they need and MassDriver will “grab infrastructure as code modules and policy as code modules from the registry and start to provision using that.”

Drawing infrastructure makes it easy to spot the components of cloud environments and understand how they work together. “When you’re in MassDriver”, Cory explains, “you just drag your application out and it does the Terraform for you, does the role assignments, binds it to your Kubernetes workload.”

In this way, MassDriver balances developer flexibility with robust governance. It empowers developers to make architectural changes while abstracting away granular control over credentials, module selection, and tool inputs. MassDriver handles these underlying details, making it ideal for users who have a defined set of architectural requirements but lack the specific skills to implement them.

To achieve this flexibility and extensibility, MassDriver is built on a functional programming model with types, using a type system as its extension mechanism. As Cory explains, “If you’re using, let’s say Mongo Atlas, we don’t have a type for Mongo Atlas in MassDriver by default, but you can push new types into your registry.” Infrastructure code is encapsulated within typed “bundles.” Each bundle defines input types (configuration parameters), output types (results/artifacts), and the underlying infrastructure code, which is treated as a black box. Consequently, MassDriver itself “has no idea what it’s doing”; it simply understands types and their relationships.

Multi-layered testing strategyGiven the complexity of infrastructure deployments, MassDriver employs a multi-layered testing strategy. This strategy addresses the limitations Cory identifies in traditional testing approaches within the DevOps community. The DevOps community needs to be “a bit more test minded,” he explains; “One of the things I think is really missing from our community and the DevOps community is being a bit more test minded.”

Integration testingCory points out that tools like Terraform test often focus on unit testing individual resources, which doesn’t adequately capture the interconnected nature of real-world infrastructure. Testing a single component in isolation, such as a PostgreSQL database, fails to account for crucial dependencies like the VPC and other necessary configurations.

Therefore, MassDriver emphasizes integration testing, which tests different components together to ensure their cohesive functionality within a system—a more accurate reflection of production environments. As Cory suggests, Terraform test is better suited for validating code correctness rather than testing the interactions between different infrastructure components.

Black box testingSince MassDriver treats all components as black boxes, at its core, it tests that inputs conform to the expected types and verifies that the resulting outputs adhere to them and their structures. This simplifies testing by focusing on system behavior rather than internal implementation details.

Provisioning testingOn the other hand, through provisioning testing, MassDriver ensures infrastructure setups function correctly in real environments, validating credential type interactions and compatibility with various published credential types. When infrastructure is provisioned, MassDriver captures the resulting outputs—such as configuration details, connection information, or resource identifiers—as Artifacts. For example, provisioning a database might result in an Artifact containing the connection string, port, and credentials.

Artifacts can then be used by other infrastructure components, creating a modular and reusable system where the output of one module becomes the input for another, effectively managing dependencies. A web application module, for instance, might rely on the database connection details (an Artifact) from a database module. Artifacts also facilitate infrastructure state tracking and versioning, enabling rollback to previous configurations and providing a change history for more robust management. In testing, Artifacts serve as known good outputs to validate the functionality of other components, ensuring that changes in one area don’t negatively impact others.

TerraTest for infrastructure codeTo test their infrastructure’s code, MassDrive uses Gruntworks’ TerraTest, a popular testing framework for infrastructure as code. TerraTest automates the deployment and management of cloud resources within the test suite, verifies that provisioned resources meet expected criteria, and applies load and simulates failure conditions to ensure robustness.

Use-case-driven test casesBecause isolated component testing isn’t feasible, MassDriver uses use-case-driven test cases. Each infrastructure module is designed around specific use cases “that we design each module around,” such as deploying a database in a VPC with specific configurations. These use cases define the required inputs and expected outputs, which are then tested to ensure correct module behavior in real-world scenarios.

Additionally, MassDriver Github has example configurations for each module. For one thing, these examples provide users with pre-configured setups to quickly deploy infrastructure without having to understand every detail. However, these are used as well to test cases and validate the infrastructure modules, ensuring these modules work as expected in various configurations.

Dependencies testingTo test dependencies, MassDriver sets up complete environments with all necessary components, ensuring each piece of infrastructure works correctly within its context. Before testing a specific component, MassDriver uses its own platform to automatically provision required downstream dependencies. For example, testing a PostgreSQL database would first involve setting up the necessary VPC, subnets, and security groups. After provisioning dependencies, MassDriver applies a load to test how well the infrastructure handles real-world usage, such as applying load to a production database to test auto-scaling behavior. By provisioning a full stack of dependent services (e.g., a database, VPC, and application server), MassDriver performs integration tests to verify that all components work together correctly and that changes in one area don’t break the overall system.

Performance optimizationFinally, recognizing that base provisioning can significantly lengthen test completion times, MassDriver employs strategies like parallelizing tasks and adjusting configurations to mitigate this. They also use data approximations for certain tests, such as using smaller data sizes for database restores, to expedite complex operations.

License changes in open sourceOrganizations often begin using open-source software due to its free availability without initial financial commitments. This allows them to experiment, integrate the software into their systems, and assess its suitability before making potential investments or facing licensing restrictions.

In some cases, on the other side, developing the open source software, there are companies looking to eventually profit from it. As Cory puts it, “open source, by and large, is the purchase pathway for many enterprises.” Initially, enterprises often adopt open-source software due to its free availability and ease of initial integration. However, as their reliance on the software grows, they frequently recognize the value of the additional features, support, and stability offered by enterprise or paid versions.

This strategy is frequently employed by companies developing open-source software. The initial goal is to achieve widespread adoption through the free version, establish a user base, and demonstrate the software’s capabilities. Once enterprises become dependent on the software within their critical workflows, these companies can then offer a conversion path to paid, enterprise-grade versions. These paid versions typically include valuable enhancements such as dedicated support, improved scalability to handle larger workloads, enhanced security features, and compliance certifications.

Still, despite these might seem compelling reasons for enterprises to upgrade, many enterprises start by using open-source software for free over an extended period. They integrate it into their workflows, benefit from its functionality, and often delay transitioning to the paid or enterprise version until they are done dealing with the difficulty of managing the open-source version. Eventually, some enterprises decide to pay for the software, either reluctantly because they need additional features, support, or stability, or sometimes willingly when they recognize the value the software brings. However, most of them continue to use the free version.

According to Cory, when “99% of the people that use our thing use it for free, you’re incentivized to change a licensing model.” This pattern of delayed payment creates a situation where open-source software companies rely heavily on converting free users into paying customers. “You’re incentivized to make the open-source version worse,” he concludes. Consequently, companies initially releasing software under an open-source license and later changing that license impair those that have integrated their software into their operations.

As a result, Cory advocates for certain highly prevalent open-source projects to be managed by foundations rather than single organizations. He believes “there’s a point where something becomes so ubiquitous across our industry that it should not be controlled by a single organization.” Placing these projects under the stewardship of a foundation would prevent unilateral license changes and ensure the software remains community-driven, stable, and less susceptible to the whims of a single entity.

Moreover, Cory believes developers should be aware of this situation and ask themselves about the ownership of their tooling: “We should be opening an issue on their GitHub and saying, ‘Why isn’t this a part of the foundation?’ ” In this regard, he also believes there should be more “smaller foundations centered around specific technologies” and with a smaller scope.

DevOps relevanceCory points out that DevOps teams often “don’t get the same budget as developers and are seen as a burden rather than enablers of business value.” He articulates the difficulty in demonstrating their impact, stating that “it’s hard to see what success we add to the business and the value that we create.”

Cory argues that DevOps teams need more budget to acquire platform tooling earlier, rather than building these tools in-house. This budget constraint forces DevOps teams to often rely on free versions of tools for longer periods before justifying or affording paid versions. Since DevOps is responsible for maintaining and optimizing infrastructure, this delay can hinder their operations. They may spend significant time managing and troubleshooting free tools instead of leveraging more robust, supported enterprise versions that could streamline their workflows. Cory suggests that if DevOps teams had the resources to adopt the right tools earlier, they could deliver greater business value by focusing on higher-level tasks rather than managing basic infrastructure issues.

The bottom lineFollow Cory O’Daniel on X at @coryodaniel and Linkedin to learn the latest on platform engineering. You can also listen to his Platform Engineering Podcast.

To learn more about OpenTufo, visit OpenTofu.org and follow @opentofuorg on X and Linkedin.

The post Cory O’Daniel on Infrastructure as Code appeared first on Semaphore.

View Details

Continuous Delivery (CD) transforms how teams ship software by automating the path to production, making releases routine, predictable, and stress-free. With faster delivery cycles and improved quality, CD enables teams to provide users with reliable value.

This guide offers a 10-step checklist for implementing CD effectively, featuring concrete examples, actionable insights, and best practices. Whether you’re starting fresh or refining your processes, this resource will help you build a robust, high-performing delivery pipeline.

The 10-Step Checklist1. Adopt Version Control Best PracticesVersion control is the backbone of modern software development, enabling teams to collaborate effectively and maintain a history of code changes.

  • Use Systems Like Git: Implement a distributed version control system like Git to manage your codebase. Git allows multiple developers to work independently and merge their changes seamlessly. It provides robust features for branching, merging, and reverting changes, making it suitable for projects of any size.
  • Implement trunk-based development (GitFlow): Establish a branching strategy that aligns with your workflow, such as Gitflow, feature branching, or trunk-based development. Use branches to isolate work on new features or bug fixes.
  • Enforce pull requests and code reviews: Incorporate code reviews into your process by utilizing pull requests on platforms like GitHub or Bitbucket. Code reviews enhance code quality, encourage knowledge sharing, and promote team collaboration.
  • Maintain clean commit history with conventional commits: Use a standardized commit message format to indicate change type, scope, and purpose, improving readability and traceability. For example, by categorizing commits into types like “fix,” “feat,” or “chore”.

```

1. Create a short-lived feature branch from main (aka trunk)git checkout maingit pull origin maingit checkout -b feature/new-feature# 2. Implement the new feature then stage and commit changesgit add .git commit -m "feat: implement new feature"# 3. Push the feature branchgit push -u origin feature/new-feature# 4. Create a pull request (PR) on GitHub/GitLab/Bitbucket# - Request reviews from team members# - Address any feedback by making additional commits# 5. Once approved, merge your changes back into main (the trunk)# - Optionally rebase or squash before merging to keep history cleangit checkout maingit pull origin maingit merge feature/new-feature# 6. Push merged changes to maingit push origin main

``` 2. Set Up Continuous Integration (CI)Continuous Integration is crucial for detecting issues early and ensuring that code changes integrate smoothly into the main codebase.

  • Automate Builds and Tests Using CI Tools: Utilize a CI platform like Semaphore to automate your build and test processes. Semaphore allows you to define workflows consisting of pipelines, which are sequences of tasks to build, test, and deploy your application. You can create pipelines using the visual workflow editor or by writing YAML configuration files for greater control and versioning. For instance, you can set up a pipeline that triggers whenever code is pushed to a branch or a pull request is opened. In your pipeline configuration, you specify the steps needed to build your application and run your test suite.
  • Provide Immediate Feedback to Developers: Configure your CI system to report build and test results promptly. Semaphore integrates with version control systems to provide status checks on commits and pull requests. Immediate feedback allows developers to address issues quickly, maintaining code quality and reducing the likelihood of defects reaching production. Semaphore’s integration capabilities ensure that developers are notified of build failures or test issues as soon as they occur, fostering a proactive development environment.

  • Implement Automated TestingAutomated testing ensures that code changes do not introduce regressions and that your application behaves as expected.

  • Write Unit, Integration, and End-to-End Tests: Develop a comprehensive testing strategy that includes different levels of testing. Unit tests check individual components, integration tests verify the interaction between components, and end-to-end tests simulate real user scenarios. This layered approach helps catch issues at various stages.

  • Automate Tests to Run on Every Code Change: Configure your CI pipeline to run tests automatically whenever code is pushed or a pull request is opened. Tools like Semaphore allow you to define jobs within your pipeline that execute your tests. For example, in a JavaScript project using Jest, you can set up a job that runs npm test as part of your pipeline.

Example pipeline configuration for running automated tests on push and pull requests:

name: CI Pipelineon: [push, pull_request]jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Install dependencies run: npm ci - name: Run tests run: npm test - name: Build run: npm run build 4. Manage Dependencies EffectivelyProper dependency management ensures that your application builds and runs consistently across different environments.

  • Cache Dependencies: In CI/CD pipelines, caching dependencies can drastically speed up builds. Caches eliminate the need to fetch packages from scratch on every run, saving time and network bandwidth. Ensure the cache invalidates properly when the lockfile changes to avoid stale or inconsistent installs.

Example of caching dependencies in a pipeline:

name: CI Pipeline# Define the default agent (machine and operating system) for all jobsagent: machine: type: e1-standard-2 # Use a standard Linux machine with 2 CPU cores and 4GB RAM os_image: ubuntu2004 # Use Ubuntu 20.04 as the operating system image# Define the sequence of tasks to be executed in the pipelineblocks: - name: Install Dependencies # Descriptive name of the block task: prologue: commands: - checkout # Check out the code from the version control repository - cache restore # Restore cached 'node_modules' directory if available jobs: - name: Install Dependencies # Name of the job within this block commands: - npm install # Install Node.js dependencies specified in package.json epilogue: always: commands: - cache store # Cache the 'node_modules' directory for future pipeline runs * Use lockfiles: Employ tools specific to your programming language to manage libraries and packages. For example, use npm or Yarn for JavaScript projects, pip for Python, or Maven/Gradle for Java. These tools help you specify exact versions of dependencies, ensuring consistency and reproducibility. * Keep Dependencies Updated and Secure: Regularly update your dependencies to benefit from the latest features and security patches. Automate this process using tools like Dependabot or Renovate, which can create pull requests when new versions are available. Additionally, use security auditing tools such as npm audit or Snyk to detect vulnerabilities in your dependencies.

Example dependency management scripts with npm:

{ "name": "my-app", "version": "1.0.0", "scripts": { "audit": "npm audit", "outdated": "npm outdated", "update": "npm update" }, "dependencies": { "express": "^4.18.0", "react": "^18.2.0" }} * npm audit alerts you to known vulnerabilities. * npm outdated shows which packages are behind the latest release. * npm update automatically updates dependencies within their defined semver range.

  1. Automate Deployment ProcessesAutomating deployment processes ensures that software releases are consistent, repeatable, and less prone to human error.

  2. Utilize Infrastructure as Code (IaC): Implement tools like Terraform, Ansible, or CloudFormation to define and manage your infrastructure through code. IaC allows you to version control your infrastructure configurations alongside your application code, ensuring that environments can be recreated or scaled reliably. By integrating IaC into your CI/CD pipeline, you can automate the provisioning and configuration of environments needed for testing, staging, and production. Semaphore pipelines can include jobs that execute IaC scripts, enabling you to manage infrastructure changes as part of your deployment process.

Infrastructure as Code example using Terraform:

resource "aws_instance" "web" { ami = "ami-0c55b159cbfafe1f0" instance_type = "t2.micro" tags = { Name = "production-web-server" Environment = "production" }} * Maintain Consistent Environments Across Stages: Ensure that your development, testing, staging, and production environments are as similar as possible. Consistency reduces the risk of environment-specific issues and simplifies debugging. Containerization tools like Docker can help achieve consistency by packaging your application and its dependencies into portable containers. In your Semaphore pipeline, you can use Docker containers to run your application in the same environment at every stage. * Enable Zero-Downtime Deployments: An automated deployment strategy such as rolling updates or blue-green deployments can avoid service interruptions. By seamlessly transitioning user traffic between the old and new versions, you ensure uninterrupted service. Tools like Kubernetes or AWS’s Elastic Container Service offer built-in mechanisms for orchestrating zero-downtime rollouts.

By automating your deployment processes and using tools like IaC and Docker, you ensure that each deployment is performed in a controlled and consistent manner. In the next step, we’ll establish a multi-stage pipeline to ensure continuous delivery.

  1. Establish Continuous Delivery PipelinesA well-defined pipeline automates the flow of code changes from commit to deployment, ensuring that every change is tested, validated, and ready for release.

  2. Define clear stage progression: Outline the stages your code should pass through before reaching production. Common stages include build, test, staging, and production. Each stage can have its own set of checks and balances to ensure code quality and readiness. In Semaphore, pipelines are defined using YAML configuration files. You can specify the sequence of blocks (stages), dependencies, and the conditions under which each block should run.

  3. Automate Gating & Promotions: As each stage (e.g., test, staging) completes successfully, you can set up automatic or manual “gates” that determine when to promote the application to the next stage. For instance, after all tests pass in the staging environment, you can automatically trigger a production deployment. This ensures only tested, stable code is promoted.

Multi-stage pipeline example with gating and promotions:

name: CI/CD Pipelineagent: machine: type: e1-standard-2 os_image: ubuntu2004blocks: - name: Build task: jobs: - name: Compile Code commands: - checkout - ./build.sh - name: Test dependencies: - Build task: jobs: - name: Run Unit Tests commands: - ./run_tests.sh - name: Deploy to Staging dependencies: - Test task: jobs: - name: Deploy to Staging commands: - ./deploy.sh staging - name: Deploy to Production dependencies: - Deploy to Staging run: when: "branch = 'master'" task: jobs: - name: Deploy to Production commands: - ./deploy.sh production * Configure Pipelines in Tools Like Semaphore: Use Semaphore’s visual workflow editor or YAML configuration to set up your pipelines. Semaphore allows you to define triggers, conditions, and promotions to control the flow of your pipeline.

  1. Integrate Monitoring and LoggingIntegrating monitoring and logging into your CD pipeline ensures that you can track application performance, detect issues early, and respond proactively.

  2. Provide a Single Pane of Glass with Unified Dashboards: Consolidate logs, metrics, traces, and alerts into one dashboard for end-to-end observability. Many platforms (e.g., Datadog, Grafana, or Kibana) can bring together data from multiple sources, giving your team an at-a-glance view of system health. This “one pane of glass” approach reduces context-switching and speeds up root-cause analysis.

  3. Set Up Centralized Logging and Alerts: Collect logs from all parts of your application into a centralized system using tools like ELK Stack (Elasticsearch, Logstash, Kibana), Splunk, or Fluentd. Centralized logging makes it easier to search, analyze, and correlate logs from different services. Configure alerts to notify your team when certain thresholds are crossed or errors occur. This proactive approach allows you to address issues before they impact users significantly.
  4. Utilize Trace Sampling to Optimize Data Storage: Implement distributed tracing with tools like Jaeger, Zipkin, or OpenTelemetry to trace requests across microservices. However, storing every trace can be expensive and unwieldy. Trace sampling helps manage the volume of data by capturing only a fraction of all requests—allowing you to retain critical visibility into complex issues while keeping storage costs in check.
  5. Implement Performance Monitoring: Use monitoring tools to track the health and performance of your application in real-time. Tools like Prometheus, Grafana, New Relic, or Datadog can collect metrics on CPU usage, memory consumption, response times, and more. By integrating performance monitoring into your deployment process, you can automatically roll back deployments if certain metrics indicate a problem. Additionally, monitoring helps you understand how new code changes affect the application’s performance.

Example monitoring setup using Prometheus Node.js client:

const client = require('prom-client')const counter = new client.Counter({ name: 'http_requests_total', help: 'Total HTTP requests', labelNames: ['method', 'path', 'status']})app.use((req, res, next) => { res.on('finish', () => { counter.inc({ method: req.method, path: req.path, status: res.statusCode }) }) next()}) By integrating monitoring and logging into your CD pipeline, you enhance the observability of your application, enabling your team to maintain high availability and performance.

  1. Ensure Security and ComplianceSecurity and compliance are critical components of a robust Continuous Delivery pipeline. Integrating security practices early and throughout your pipeline helps prevent vulnerabilities from reaching production and ensures that your software meets regulatory requirements.

  2. Integrate Security Scans into Pipelines: Incorporate automated security scanning tools into your CI/CD pipelines to detect vulnerabilities in code, dependencies, and configurations. In Semaphore, you can add security scan jobs to your pipelines. For example, you might use tools like Snyk, OWASP ZAP, or SonarQube to scan for known vulnerabilities. By integrating security scans into your pipeline, you ensure that vulnerabilities are identified and addressed before code is deployed.

  3. Automate Compliance Checks and Manage Secrets Securely: Automate compliance checks to ensure adherence to industry regulations and standards, such as GDPR, HIPAA, or PCI DSS. Semaphore allows you to manage secrets securely through its Secrets feature. You can store sensitive information like API keys, passwords, and certificates, and inject them into your jobs as environment variables or files without exposing them in your codebase.

Integrating security and compliance into your pipeline reduces risks and helps maintain user trust.

  1. Optimize for Performance and ScalabilityOptimizing your application for performance and scalability ensures that it can handle growth and provide a smooth user experience under varying loads. Include performance and load testing in your pipeline to identify bottlenecks and ensure that new code doesn’t degrade performance. Use tools like JMeter, Gatling, Grafana k6, or Locust to simulate user traffic and measure how your application performs under stress.

Design your infrastructure to scale efficiently. Utilize cloud features like auto-scaling, load balancing, and resource allocation. In Semaphore, you can automate the scaling of your infrastructure using IaC tools like Terraform or Kubernetes. Automating infrastructure scaling ensures that your application can handle increased demand without manual intervention.

  1. Foster a Culture of Continuous ImprovementThe success of Continuous Delivery relies on the people and culture within your organization. Encouraging continuous learning and improvement ensures that your processes evolve and adapt over time.

  2. Hold Regular Retrospectives: Schedule regular meetings for the team to reflect on what is working well and what can be improved. Use insights from your CI/CD pipeline metrics, such as build times and failure rates, to inform discussions. Example topics for retrospectives include pipeline efficiency and bottlenecks, test coverage and reliability, deployment success rates, and feedback from stakeholders

  3. Encourage Ongoing Learning and Adoption of Best Practices: Promote a culture of knowledge sharing and professional development. Provide resources for learning, such as training sessions, workshops, and access to relevant literature. Encourage team members to stay updated on industry trends and to experiment with new tools and methodologies.

By fostering a culture of continuous improvement, your team remains agile, innovative, and better equipped to deliver high-quality software.

ConclusionImplementing Continuous Delivery is an iterative process. Start with the basics and gradually enhance your pipeline based on team needs and capabilities. Focus on automation, reliability, and feedback loops to build a delivery system that enables rapid, confident releases.

Here’s a summary of the 10-step checklist:

  1. Adopt Version Control Best Practices
  2. Implement Automated Testing
  3. Set Up Continuous Integration
  4. Manage Dependencies Effectively
  5. Automate Deployment Processes
  6. Establish Continuous Delivery Pipelines
  7. Integrate Monitoring and Logging
  8. Ensure Security and Compliance
  9. Optimize for Performance and Scalability
  10. Foster a Culture of Continuous Improvement

Start by evaluating your current processes against this checklist. Identify areas where improvements can be made and prioritize them based on impact and feasibility. Implement changes incrementally, involving your team in planning and execution to ensure buy-in and smooth transitions.

Additional Resources:

  • The Continuous Delivery Book
  • Google’s DevOps Research
  • The Twelve-Factor App
  • DevOps Periodic Table
  • Semaphore Tutorials:

Ready to take your Continuous Delivery pipeline to the next level? Try Semaphore for efficient and reliable CI/CD workflows. Sign up today and accelerate your path to seamless software delivery.

The post The 10-Step Checklist for Continuous Delivery appeared first on Semaphore.

View Details

2024 has been a landmark year for tech: the AI hype train is at full steam, with new LLM models emerging and Apple finally entering the race. This year has brought a whirlwind of updates: from fresh versions of frameworks and runtimes to monopoly accusations against Google over Chrome. Is 2025 the year we see quantum computers become mainstream? This is the 2024 tech news roundup.

Making AI profitableThe release of ChatGPT in 2022 sparked a chain reaction that continues unabated. With billions of dollars invested in training models, companies are racing to discover sustainable business models.

OpenAI new models
With a valuation of $157 billion but less than $2 billion in revenue, OpenAI is under pressure to generate returns on its massive investments. This year, OpenAI launched GPT-4o and the more advanced o1 model.

Part of this strategy includes introducing new plans, such as the ChatGPT Pro plan, which grants access to GPT-o1’s professional version. Whether the model justifies its $200-per-month price tag remains to be seen (and if that seems steep, don’t even think about a subscription to Devin).

In addition, OpenAI has partially released Sora, the text-to-video AI generator that was teased earlier this year. Sora can create 20-second videos in 1080p without sound, which suggests how compute-heavy AI video generation is.

Apple Intelligence
Apple has been bidding on its time to adopt AI. This year, the company introduced Apple Intelligence, which has full on-device capabilities.

While Apple’s AI offerings remain modest, including improved proofreading, summarization, enhanced Siri suggestions, and image generation, one standout feature has captured attention: the iPad now boasts an official calculator app, and it’s surprisingly impressive.

Meta’s new LlamaMeta released its latest Llama 3.2 large language model in September. The largest models are multimodal, supporting image reasoning, while the smallest are efficient enough to run on modest machines and phones.

The Llama 3.2 family can be downloaded from HuggingFace or llama.com. Llama remains the closest alternative to an open-source LLM, offering a foundation for AI applications beyond mere OpenAI wrappers.

Anthropic Claude can control your machineAnthropic continues to refine its large language models. Its latest innovation, Computer Use, allows Claude to control your device: viewing your screen, moving the mouse, and typing.

This is as amazing as it’s terrifying, but it shows that AI has great potential to automate tasks.

AI blundersWherever you look there’s AI now. It’s on your Google searches, your phones, and your computers, and it’s even made its way into your messaging apps. Under this kind of pressure, not all AI projects have succeeded.

Two notable failures this year were the Rabbit R1 and the Humane Pin. These tech wearables, aimed at replacing smartphones, received harsh reviews and failed to deliver on their promises.

Microsoft also faced backlash with its Copilot Recall feature, which takes periodic screenshots of your computer. While its purpose—to enable an in-device memory you can query—has merit, the feature’s default activation and poor announcement left many users uneasy, evoking Orwellian undertones.

State of DevOps 2024Google released the annual State of DevOps and this year the focus of the survey has fallen predictably on AI.

The report is well worth a read, but this is the TL;DR:

  • Increased productivity but not delivery performance: While AI significantly boosts productivity, flow, and job satisfaction, it hasn’t improved software delivery performance as expected.
  • Over-reliance concerns: AI adoption correlates with less time spent on meaningful work, suggesting developers may rely too heavily on AI for complex tasks.
  • Low trust in AI-generated code: Despite widespread usage, 39.2% of respondents expressed little to no trust in AI-generated code quality. Developers feel compelled to validate and tweak AI outputs, comparing this reliance to the early days of StackOverflow.
  • Environmental concerns: Many developers are worried about the environmental impact of AI, citing increased power consumption and resource costs. Microsoft’s decision to reactivate a nuclear facility to power its AI systems has only amplified these fears.
  • AI code generation rises: By October, AI-generated code accounted for over 25% of new code at Google.

Developer highlights2024 brought us new runtimes and new versions of popular frameworks.

Deno 2.0Deno hit the 2.0 milestone this year, marking it with an impressive ad. Fully compatible with Node.js, Deno now supports NPM packages, making it an excellent time to give it a spin.

Deno 2.0 also includes:

  • A stable standard library
  • Support for private NPM registries
  • JSR, an alternative package registry for JS and TS modules
  • Monorepo support
  • A new LTS support channel

Bun on WindowsBun, the latest JavaScript runtime, has finally released a native version for Windows. Previously, Windows users needed WSL to run Bun, but that’s no longer necessary.

Key updates in the latest version of Bun:

  • Windows compatibility
  • A CSS bundler
  • Enhanced NPM compatibility
  • Bytecode compilation

React 19 gets a compiler
Following the steps of Svelte, React 19 now includes a new compiler that converts React code into efficient JavaScript, eliminating the need for manual memoization with hooks like useMemo and useCallback.

We’ve covered the new compiler in another post, so here are the highlights:

  • Automatic Memoization: The compiler automatically applies optimizations equivalent to useMemo and useCallback, reducing unnecessary re-renders and improving application performance without manual code adjustments.
  • Enhanced Performance: Transforming React code into optimized JavaScript can potentially double application performance, leading to faster load times and a more responsive user experience.
  • Automatic Code Splitting: The compiler analyzes your application and automatically splits code into optimized bundles, improving load times and ensuring efficient resource utilization.
  • Enhanced Tree Shaking: By identifying and eliminating unused code, the compiler reduces the final bundle size, leading to faster application performance.
  • Improved Server-Side Rendering (SSR): The compiler optimizes components for SSR, resulting in faster initial page loads and better SEO performance.
  • Support for New Syntax and Features: It enables the use of upcoming JavaScript and React features, ensuring your application stays up-to-date with the latest advancements.

Framework releases in 2024This year a lot of popular frameworks got updated:

  • Angular 19: introduced experimental zoneless change detection support and server-side rendering improvements
  • Laravel 11: brought new features and improvements, including a first-party web socket server called Laravel Reverb, enhancing real-time capabilities for applications.
  • Remix 2.10: continued to enhance its full-stack web framework capabilities, focusing on server-side rendering and client-side routing.
  • Svelte 5: added Runes, a new signal-powered reactivity API that serves as the core of Svelte 5. This version includes sv, a brand-new CLI to manage Svelte projects.
  • Vue.js 4: introduced a new Composition API, improved TypeScript support, and enhanced performance optimizations.
  • Django 4.2: continued to build upon its high-level Python framework, promoting rapid development and clean design, with updates that enhance scalability and security for web applications.
  • Ruby on Rails 8: introduces several significant features and enhancements aimed at empowering developers to deploy and manage applications without relying on the cloud.
  • Next.js 15: introduces React 19 support, new APIs, turbo pack improvements, and improvements on caching.

Google continues to make the headlinesGoogle remains at the forefront of AI innovation while simultaneously grappling with regulatory challenges. You may recall that Google was the original creator of transformers, the foundation for ChatGPT and all LLMs.

AlphaProof gets a medalGoogle continues its advancements in deep learning. In 2016, Google’s AlphaGo defeated Lee Sedol in Go, the then-world champion in the sport. This year, the Alpha team focused on maths. AlphaProof and AlphaGeometry AI systems competed at the International Mathematical Olympiad, solving 4 out of 6 problems and winning the equivalent of a silver medal.

New quantum computer chipA few weeks ago, Google announced another breakthrough in quantum computing, with a new quantum chip named ‘Willow’. The new chip benchmarks are impressive: it can complete Random Circuit Sampling (RCS) tasks in under five minutes, something that would take the fastest classical supercomputers billions of years.

Google browser monopolyOn a more controversial note, U.S. regulators found Google guilty of engaging in monopoly practices related to its search and browser products. The U.S. government is pushing Google to sell Chrome, which currently holds a 67% global market share. If that happens, which company has deep enough pockets to buy a browser valued at $20 billion?

ConclusionAs the relentless march of AI continues, we are witnessing the tech industry reach new levels of maturity. Developers are making clear decisions about where they want AI—and where they don’t. Let’s hope 2025 brings a more balanced approach to AI integration.

As we look forward to another year, one thing is certain: change. It’s an exciting time to be part of the developer community. Here’s to another year of building, learning, and innovating—happy coding!

The post 2024 Tech Roundup: The News You Might Have Missed appeared first on Semaphore.

View Details

Despite the pervasiveness of open-source software, its sustainability is increasingly under threat. While some communities have established robust ownership for distributing and maintaining their software, others, despite their relevance and popularity among enterprises, are lacking adequate support. In this episode, Tembo.io Head of Education Floor Drees explains the need for a more collaborative and sustainable approach to open-source development. She will help us explore the current state of the Postgres ecosystem and the broader open-source community to point out the critical issues that need to be addressed to ensure the future of these vital technologies.

Edited TranscriptionA series of notable positions across influential tech companies characterize Floor Drees’ career. From Microsoft to Grafana Labs, and later Aiven, Floor gradually specialized in database technologies. Currently, she is the head of education at Tembo, a developer platform built on Postgres (also known as PostgreSQL) designed as a unified solution to employ data services and build database applications through its over 200 Postgres extensions.

Trusting Postgres extensionsAs a reference in the Postgres ecosystem, Tembo provides a trust-worthy interface for employing extensions otherwise scatteredly available. “It is a bit of the Wild Wild West,” Floor says, with “a lot of different extension authors hosting their extensions in very different places.”

The issue of decentralization goes further than a mere inconvenience: Each source implies different hosting and development practices to be understood, adapted, and trusted separately. For organizations considering implementing new extensions, the process is fraught with uncertainty. As Floor notes, companies become “super cautious and anxious to adopt” extensions due to the unclear provenance and reliability of these add-ons.

Unlike ecosystems like RubyGems, which offers transparency and credibility through information about downloads, contributors, and companies involved, Postgres extensions lack a comprehensive trust framework. Floor believes it is “very difficult to understand which are the tools that you can trust, rely on, comfortably put into your stack and forget about to a certain extent.”

The culprit behind Postgres’ extensions issue is the database’s own development philosophy. Unlike most open-source projects, Postgres adopts a more closed approach to contributions characterized by a steep learning curve. In this sense, contributors need to actively participate in mailing list discussions before proposing and designing changes. As a result, there’s a limited number of merge requests, which undergo rigorous review and are typically incorporated into yearly releases.

In like manner, Postgres does not use Github for contributing, as it “incentivizes sort of drive-by contributions,” says Floor, while demanding reviewing many pull requests. However, she points out that Postgres’ decision implies that “growing your contributor base with new people that might bring new perspectives is going to be harder.”

Moreover, extension creators have a diminished role in comparison with core developers. In Postgres, unlike other projects like Ruby, only those contributing to its core are considered contributors and listed as such on its website. Floor understands this is an unfair perspective, as she believes “extensions, sorting of the tooling, and having a healthy ecosystem is what makes a project grant.” “Not recognizing those types of contributions, I find that unfortunate. That’s the word I’m gonna choose, unfortunate,” she says.

The future of open source: Responsibility, legislation, and community sustainabilityBeyond Postgres, the overall open-source ecosystem demands a fundamental reimagining of how its communities collaborate and sustain themselves. At the heart of the issues is the increasingly uneven contributions, marked by a stark disparity between individual developers and massive tech companies. The situation is particularly precarious for smaller contributors. Many extensions are maintained by individual developers, even though they might support critical infrastructure used by major corporations.

As a way to achieve economic sustainability, open-source projects have started re-licensing their products to various degrees; for example, last year data platform Directus started charging large enterprises for production licenses. Floor is concerned about the disruptive potential of sudden licensing shifts and the “work and costs associated with switching out technology”: “You don’t necessarily see those coming and you need to really quickly find an alternative for potentially a really vital piece of your product to sort of like switch it out.” “Those are mostly larger projects,” she explains, “but also smaller projects too changed their license because they’re just done with bigger companies freeloading on their work.” Consequently, Floor believes more and more projects will move under the influence of foundations and, to avoid security issues or license changes, “companies will probably look at foundation-backed projects.”

The security concerns behind the sustainability issue have led governments to take notice of it. This year, the European Union’s Cyber Resilience Act (CRA) entered into force over software and hardware manufacturers and retailers to regulate and mandate cybersecurity requirements, such as reporting vulnerabilities, providing software updates, and auditing products.Floor believes the CRA is an overall step in the right direction that “might force —for lack of a better word— organizations to care a lot more” and feel “an obligation to put more resources” on the software they rely on as “they now have the obligation to go back and contribute their fixes to these projects.” “They can’t just fix it for their customers anymore; that is not allowed,” she concludes.

The bottom lineFor learning about Postgres extensions, check Tembo’s extension registry Trunk Visit Floor’s website to learn more about her work. You can follow her on:

X at @DevOpsBarbie.

Bluesky at@floord.bsky.social.

Mastodon at @floord@hachyderm.io.

The post Floor Drees on Building Sustainable Open-Source Ecosystems appeared first on Semaphore.

View Details

Yes, I already know: when developers read the word “testing,” a mix of emotions comes to their mind: fear, anxiety, frustration…

For this reason, this post aims to explain what test automation is and how developers can benefit from it, in terms of productivity and code quality.

In particular, it discusses what unit testing and integration testing are, how they differ from each other, and how to implement them in Python using Flask, by providing a step-by-step tutorial.

So, fear no more, and let’s dive right into it!

What is Test Automation?Test automation is the process of automating the testing activities developers do to ensure that their code works as expected. It involves writing scripts or programs that simulate user interactions with an application: this helps verify its behavior under different conditions, and report any issues found.

Manual VS Automated TestingManual testing is the first step developers need to overcome when they learn to code if they want to ensure their code works as expected when users will use it.

This process needs total human intervention, which means that every time you perform a new test, you’ll need to manually execute each step involved in your test case. So, as understandable, this can be very time-consuming and prone to errors due to human mistakes.

Also, the process of manual testing is generally very simple. For example, a typical use case in Python is to write some print() here and there along the code to check whether everything works fine. But, indeed, this does not ensure the code is working correctly: it only shows that the code runs without crashing until a specific point.

On the other hand, automated testing—while automating the process of testing—also provides processes and procedures that actually ensure the code works as expected.

Benefits of Test AutomationSo, let’s point out some of the benefits of using automated testing:

  • Standardization: Test automation equips developers with a standardized approach to testing by providing frameworks and processes that actually ensure the code works as expected.
  • Reduced human errors: As with every automation, test automation eliminates the human errors that can occur in the processes.
  • Increased code quality: By ensuring the code works as expected, test automation helps improve the overall quality of the code. In fact, test automation facilitates consistent and thorough testing: this reduces the number of bugs entering into production, thus improving the overall quality of the code.
  • Faster code delivery: With automated testing, developers can quickly identify and fix issues before releasing software updates. This speeds up the development cycle and allows teams to deliver high-quality products more frequently.
  • Increased test coverage: The goal of testing software is to ensure that all parts of the code are tested thoroughly (or, at least, the majority of them). While manual testing may miss certain areas or scenarios, automated tests can cover a wider range of cases and edge situations, increasing confidence in the reliability of the system when delivered to production.

Test Automation: Understanding Unit TestsIn the context of test automation, we can define a “unit” as the smallest testable piece of a software application. This means that a unit could be a function, method, class, module—or anything related—depending on how granular you want to get or depending on the software you are developing.

So, unit testing is the test automation practice that focuses on testing individual components of a program to ensure they work as intended, separately from the other parts of the same program. For this reason, this is generally the first level of testing performed during the development process.

For example, in a web application that manages online purchasing after users have logged in, a unit test can focus on the login phase, ensuring the login credentials are validated properly, and that the user is redirected to the correct page upon successful authentication.

While this testing practice does not guarantee that the entire program works as expected, it ensures that each component behaves correctly independently from the others. For this reason, this does not mean that unit tests are unuseful; instead, they provide some benefits during the software lifecycle like:

  • Early bug detection: Unit testing is often practiced by developers themselves while coding. This allows them to identify and fix bugs easily during the development phase. This early detection allows addressing issues before they become more complex and, eventually, costly.
  • Enabling frequent releases: When unit tests are implemented effectively, they help ensure that changes made to one part of the codebase don’t break other parts. This makes it easier for teams to make frequent releases without worrying about introducing new issues or regressions.
  • Simplified refactoring and maintenance: Well-written unit tests act as documentation for the codebase, making it easier for developers to understand how different parts interact. This serves as safety nets, helping teams against unintended side effects when refactoring or modifying existing functionality during maintenance tasks.

Characteristics of Effective Unit Tests: The FIRST PrincipleDue to the nature of unit testing, effective unit tests should follow the so-called FIRST principle:

  • Fast: Staring at the screen during the first of 10 unit tests running, knowing the first will take 15 minutes alone to complete, is not acceptable. Instead, unit tests should run quickly, ideally within milliseconds. This enables developers to run them frequently during the development process, allowing for rapid feedback and iteration. Fast tests, in fact, encourage developers to run them frequently, integrating them seamlessly into the development workflow with the aim to catch defects early and accelerate the development cycle.
  • Independent: Each unit test should be independent of other tests, meaning they can be executed in any order without affecting the results. This promotes parallel execution, teamwork, and efficient test suites, which is a way to speed up the testing process.
  • Repeatable: If you run a unit test today obtaining a different result than yesterday, something went wrong. Instead, unit tests should produce consistent results regardless of the environment or machine where they’re run. This ensures reliable outcomes across different setups.
  • Self-validating: A good unit test should use assertions to validate outcomes, providing a clear pass or fail based solely on its own logic result without requiring external input, interpretation, or manual intervention.
  • Timely: Ideally, tests should be written just before the production code that makes them pass. This enables the test to guide your design choices, aligning with the so-called Test-Driven Development (or TDD). To get the idea, consider that for some developers “timely” means writing tests in the same timeframe as the code itself.

Test Automation: Understanding Integration TestsIntegration testing is another important aspect of test automation that focuses on testing the interactions between multiple units of a software application. So, unlike unit tests, integration tests verify how these components work together to achieve the desired outcome as a whole.

The main idea behind integration testing is to simulate real-world usage scenarios and detect potential issues that arise when combining different parts of the application. These tests help uncover problems such as data inconsistencies, communication failures, or unexpected behaviors caused by interactions between various components.

As an example, let’s consider the previously-mentioned web application scenario. In this case, an integration test might involve simulating the entire purchasing flow, including logging in, selecting items, adding them to the cart, proceeding to checkout, and completing payment. Such a comprehensive test ensures that all aspects of the purchase process function correctly when integrated, allowing deployment to production with greater confidence.

Integration Testing MethodsThere are several methods used for integration testing, each with its own advantages and trade-offs. Here are some common approaches:

  • Big-bang: This testing method involves integrating all units simultaneously and testing them as a complete system. While this is a thorough method, it can lead to longer test cycles since fixing bugs discovered later in the process requires retesting earlier stages. For this reason, this method is typically used when the components are relatively independent and can be tested individually.
  • Top-down: Top-down integration starts with higher-level units and gradually integrates with lower-level ones. This approach helps isolate issues closer to the top levels but may overlook deeper-seated problems in lower layers. So, while this method is highly effective in the early detection of interface issues and major control points, it requires the creation of mock objects for lower-level units, which can be complex and time-consuming.
  • Bottom-up: Bottom-up integration tests start with lower-level units and progressively integrate them into higher-level ones. This method is useful for identifying issues related to internal interfaces and dependencies among components. Its great advantage is that it allows developers to test the lower-level modules and their interactions before integrating them into the higher levels, generally with no need for mock objects. However, it delays the testing of the higher-level functionalities, which may push back the identification of defects, slowing down the development process.
  • Incremental testing: Incremental testing combines the elements of the other testing approaches. It begins by testing smaller units and gradually increases complexity by integrating additional components over time when the tested ones work as expected. This method strikes a balance between early defect detection and timely completion of higher-level tests, offering flexibility and adaptability throughout the development lifecycle. However, it is time-consuming and may not be suitable when there’s the need to match tight deadlines.

Key Differences Between Unit and Integration TestingNow that we’ve covered both unit and integration testing, let’s highlight the key differences between them to provide a clearer overview:

  • Scope and focus: Unit tests concentrate on testing individual units, whereas integration tests examine how those components interact with each other. So, in other words, unit tests focus on ensuring each component functions correctly, while integration tests ensure that the combined components behave as expected as a whole.
  • Isolation vs interaction: Unit tests operate in isolation, focusing on a single unit’s behavior without considering its interactions with other components. On the contrary, integration tests evaluate how units collaborate and communicate with each other, taking into account their relationships and dependencies. To do so, they require mocking or stubbing techniques to simulate the behavior of dependent components, as users supposedly will use them.
  • Complexity and execution time: Unit tests tend to be simpler and faster to execute compared to integration tests. This is because unit tests usually involve fewer steps and less setup, while integration tests require setting up a more realistic environment that mimics actual usage scenarios.
  • Debugging and failure analysis: Debugging unit tests is generally straightforward, as they target isolated components. On the other hand, debugging integration tests can be more challenging due to the increased complexity and interdependencies between components. This is why failure analysis in integration tests may involve tracing through multiple interconnected units to pinpoint the root cause of issues.
  • Actors involved: While this topic depends on the project structure and organization, we can say that unit tests are primarily conducted by developers who create and execute them to ensure the proper functioning of individual units. Instead, as integration tests are more complex, they are generally performed by QA teams; yet, it may involve collaboration between developers and testers, as well as stakeholders representing end-users’ perspectives.

Test Automation: A Step-by-Step Python Tutorial Using FlaskAfter all this theory, it’s now time to get’s hands on code!

In this section, you will learn how to create a simple Flask application and how to test it. For the sake of simplicity, the application can be tought of as an online calculator that performs basic arithmetical operations. Specifically, it adds and multiply numbers.

But before diving into the code, let’s start by listing all you need to correctly set up your environment.

Prerequisites, Requirements, and Repository StructureBefore you begin, make sure you have Python 3.8+ installed on your computer.

Then, create a repository – I named it flask_app – with the following structure:

├── app.py├── templates/│ ├── index.html│ └── result.html└── tests/ ├── __init__.py ├── test_unit.py └── test_integration.py The venv/ folder contains a virtual environment. You can create it by typing:

python3 -m venv venv To acvitate it on Windows, type:

venv\Scripts\activate And on Linux/MacOS, type:

source ./venv/bin/activate After the virtual environment has been activated, install the required packages by typing:

pip install Flask pytest requests Now you are ready to write your code!

Building The ApplicationNow you can create your online calculator by writing the following code inside app.py:

from flask import Flask, render_template, request, redirect, url_for, jsonifyapp = Flask(__name__)def add_numbers(a, b): """Adds two numbers and returns the result.""" return a + bdef multiply_numbers(a, b): """Multiplies two numbers and returns the result.""" return a * b@app.route('/')def index(): """Render the main page with forms.""" return render_template('index.html')@app.route('/calculate', methods=['POST'])def calculate(): """Handle form submission and display the result.""" operation = request.form.get('operation') a = request.form.get('a') b = request.form.get('b') c = request.form.get('c') try: a = float(a) b = float(b) # Perform the selected operation if operation == 'add': result = add_numbers(a, b) return render_template('result.html', result=result) elif operation == 'multiply': result = multiply_numbers(a, b) return render_template('result.html', result=result) elif operation == 'add_multiply': c = float(c) sum_result = add_numbers(a, b) result = multiply_numbers(sum_result, c) return render_template('result.html', result=result) else: return render_template('result.html', error="Invalid operation selected.") except (TypeError, ValueError): return render_template('result.html', error="Invalid input provided.")@app.route('/add', methods=['GET'])def add(): """API endpoint to add two numbers.""" try: a = float(request.args.get('a')) b = float(request.args.get('b')) result = add_numbers(a, b) return jsonify({'result': result}) except (TypeError, ValueError): return jsonify({'error': 'Invalid input'}), 400@app.route('/multiply', methods=['GET'])def multiply(): """API endpoint to multiply two numbers.""" try: a = float(request.args.get('a')) b = float(request.args.get('b')) result = multiply_numbers(a, b) return jsonify({'result': result}) except (TypeError, ValueError): return jsonify({'error': 'Invalid input'}), 400@app.route('/add_multiply', methods=['GET'])def add_and_multiply(): """API endpoint to add two numbers and then multiply the result by a third number.""" try: a = float(request.args.get('a')) b = float(request.args.get('b')) c = float(request.args.get('c')) sum_result = add_numbers(a, b) final_result = multiply_numbers(sum_result, c) return jsonify({'result': final_result}) except (TypeError, ValueError): return jsonify({'error': 'Invalid input'}), 400if __name__ == '__main__': app.run(debug=True) So, this code created an app that does the following:

  • Adds two numbers (with the add function).
  • Multiply two numbers (with the multiply function).
  • Creates three endpoints that can be usable as a web app. One endpoint sums numbers (the add endpoint), one multiply numbers (the multiply endpoint), and the last one adds two numbers and multiplies the result by a third number (with the add_multiplyendpoint). Each endpoint manages the expected data types and errors, in case of bad inputs.

To make it locally working, you also need to write the following code into the index.html file:

Calculator App Calculator App Add Two Numbers First Number (a): Second Number (b): Add Multiply Two Numbers First Number (a): Second Number (b): Multiply Add Two Numbers and Multiply the Result First Number (a): Second Number (b): Multiplier (c): Calculate This will manage the UI of the app.

Also, you need to add the following code into the result.html file:

``` Calculation Result Calculation Result {% if result is defined %} The result is: {{ result }}

{% elif error is defined %}        Error: {{ error }}

{% endif %}    Perform another calculation

``` This will manage the UI of the result page.

Now you can run the app by typing:

python app.py and open your browser at http://localhost:5000/ to see it live.

You are now ready to test it!

Manual TestingFor the sake of completeness, let’s briefly discuss how to manually test this application.

For each of the three endpoints, you should manually:

  • Verify all the possible acceptable cases. This means testing numbers, which means: integers and floating (as well as negative numbers, null values, etc…).
  • Verify all the possible unacceptable cases. This means testing strings, which means: letters, symbols, etc. This also means that the application should return an error message to say that the input is invalid, not break the code.

For brevity, let’s just manually verify one endpoint. For example, let’s test the add one.

In the positive case, you should insert two numbers:

When clickinc on add, you should be redirected to another page showing the result of the addiction:

If you now insert a letter instead of a number, you should receive an error message:

As understandable, even in the case of a basic app, the effort when manually testing code takes a lot of time and resources.

So, let’s now use automated testing to save our time!

Unit TestsTo create automated tests based on unit testing, you can consider the add_numbers() and multiply_numbers() functions to be units. In this scenario, you can write the following code into the test_unit.py in the tests/ folder:

import unittestimport sysimport os# Add the parent directory to sys.path to import app.pysys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))from app import add_numbers, multiply_numbersclass TestMathFunctions(unittest.TestCase): """Unit tests for math functions with broad test cases.""" def test_add_numbers_valid(self): """Test the add_numbers function with valid numeric inputs.""" # Test with positive integers self.assertEqual(add_numbers(1, 2), 3) # Test with negative integers self.assertEqual(add_numbers(-1, -1), -2) # Test with zero self.assertEqual(add_numbers(0, 0), 0) # Test with positive floats self.assertEqual(add_numbers(1.5, 2.5), 4.0) # Test with negative floats self.assertEqual(add_numbers(-1.5, -2.5), -4.0) # Test with mixed integer and float self.assertEqual(add_numbers(1, 2.5), 3.5) # Test with very large numbers self.assertEqual(add_numbers(1e20, 1e20), 2e20) # Test with very small numbers self.assertAlmostEqual(add_numbers(1e-20, 1e-20), 2e-20) def test_add_numbers_invalid(self): """Test the add_numbers function with invalid (non-numeric) inputs.""" # Test with strings (letters) with self.assertRaises(TypeError): add_numbers('a', 'b') # Test with strings that look like numbers with self.assertRaises(TypeError): add_numbers('1', '2') # Test with None with self.assertRaises(TypeError): add_numbers(None, 2) # Test with lists with self.assertRaises(TypeError): add_numbers([1, 2], 3) # Test with dictionaries with self.assertRaises(TypeError): add_numbers({'a': 1}, {'b': 2}) # Test with boolean values with self.assertRaises(TypeError): add_numbers(True, False) def test_multiply_numbers_valid(self): """Test the multiply_numbers function with valid numeric inputs.""" # Test with positive integers self.assertEqual(multiply_numbers(2, 3), 6) # Test with negative integers self.assertEqual(multiply_numbers(-2, -3), 6) # Test with positive and negative integers self.assertEqual(multiply_numbers(-2, 3), -6) # Test with zero self.assertEqual(multiply_numbers(0, 100), 0) # Test with positive floats self.assertEqual(multiply_numbers(2.5, 4), 10.0) # Test with negative floats self.assertEqual(multiply_numbers(-2.5, -4), 10.0) # Test with mixed integer and float self.assertEqual(multiply_numbers(3, 0.5), 1.5) # Test with very large numbers self.assertEqual(multiply_numbers(1e10, 1e10), 1e20) # Test with very small numbers self.assertAlmostEqual(multiply_numbers(1e-10, 1e-10), 1e-20) def test_multiply_numbers_invalid(self): """Test the multiply_numbers function with invalid (non-numeric) inputs.""" # Test with strings (letters) with self.assertRaises(TypeError): multiply_numbers('a', 'b') # Test with strings that look like numbers with self.assertRaises(TypeError): multiply_numbers('1', '2') # Test with None with self.assertRaises(TypeError): multiply_numbers(None, 2) # Test with lists with self.assertRaises(TypeError): multiply_numbers([1, 2], 3) # Test with dictionaries with self.assertRaises(TypeError): multiply_numbers({'a': 1}, {'b': 2}) # Test with boolean values with self.assertRaises(TypeError): multiply_numbers(True, False)if __name__ == '__main__': unittest.main()

NOTE: The use of the sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) is to allow importing from the parent directory (..) where the app.py file resides. This is necessary because the current script is located within the tests/ subdirectory, and thus needs to navigate upwards to access files outside its immediate scope.

As you can see, with only a few lines of code, this test file takes care of different possibilities like inserting negative numbers, very big numbers, letters, and more.

Now, if you go into the tests/ folder and launch the test_unit.py file by tiping python3 test_unit.py you should receive an output like this one:

Ran 4 tests in 0.001sFAILED (failures=2) So, in this case, you have two tests failed. Why has this happended? Return up to the previous code; as you can see, it reports various functions that represent different cases; let’s consider two of them:

  • The test_add_numbers_valid() function creates and tests positive cases for the add_numbers() function. This means that the values tested are acceptable ones.
  • The test_add_numbers_invalid() function, on the other hand, creates and tests negative cases for the add_numbers() function. This means that the values tested are not acceptable ones.

So, a question may arise now: have you managed unacceptable values in the functions in the app.py file? Well, the answer is no! In fact, if you scroll the terminal, you should see an output like this one:

AssertionError: TypeError not raised This happens because the add_numbers() and multiply_numbers() functions in the app.py are not raising a TypeError when provided with invalid (non-numeric) inputs, as your tests expect.

Hooray! Here’s another big result achieved! Not only you can test multiple scenarios automatically, but you can also find ways to improve your code. This is why the Test-Driven Development approach is so powerful!

So, if you want to improve your code in the app.py by also considering to raise a TypeError with invalid inputs, you can modify the functions like so:

def add_numbers(a, b): """Adds two numbers and returns the result.""" if isinstance(a, bool) or isinstance(b, bool): raise TypeError("Boolean values are not allowed") if not isinstance(a, (int, float)) or not isinstance(b, (int, float)): raise TypeError("Both inputs must be int or float") return a + bdef multiply_numbers(a, b): """Multiplies two numbers and returns the result.""" if isinstance(a, bool) or isinstance(b, bool): raise TypeError("Boolean values are not allowed") if not isinstance(a, (int, float)) or not isinstance(b, (int, float)): raise TypeError("Both inputs must be int or float") return a * b At this point, you should get no more errors and the result should be something like this one:

Ran 4 tests in 0.001sOK Which means that 4 test have been performed in 0.001 seconds and everything went fine!

Integration TestingLet’s now use a botton-up approach to perform integration testing. In this scenario, you can write the following code into the integration_unit.py in the tests/ folder:

import unittestimport sysimport os# Add the parent directory to sys.path to import app.pysys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))from app import appclass TestAppIntegration(unittest.TestCase): """Integration tests for the Flask application.""" def setUp(self): """Set up the test client.""" # Configure the app for testing app.config['TESTING'] = True self.client = app.test_client() def test_add_endpoint(self): """Test the /add endpoint with valid and invalid inputs.""" # Valid input test response = self.client.get('/add?a=10&b=20') self.assertEqual(response.status_code, 200) self.assertEqual(response.get_json(), {'result': 30.0}) # Invalid input test (non-numeric values) response = self.client.get('/add?a=foo&b=bar') self.assertEqual(response.status_code, 400) self.assertEqual(response.get_json(), {'error': 'Invalid input'}) # Missing parameter test (missing 'b') response = self.client.get('/add?a=10') self.assertEqual(response.status_code, 400) self.assertEqual(response.get_json(), {'error': 'Invalid input'}) def test_multiply_endpoint(self): """Test the /multiply endpoint with valid and invalid inputs.""" # Valid input test response = self.client.get('/multiply?a=5&b=4') self.assertEqual(response.status_code, 200) self.assertEqual(response.get_json(), {'result': 20.0}) # Invalid input test (non-numeric value for 'b') response = self.client.get('/multiply?a=5&b=bar') self.assertEqual(response.status_code, 400) self.assertEqual(response.get_json(), {'error': 'Invalid input'}) # Missing parameter test (missing 'b') response = self.client.get('/multiply?a=5') self.assertEqual(response.status_code, 400) self.assertEqual(response.get_json(), {'error': 'Invalid input'}) def test_add_multiply_endpoint(self): """Test the /add_multiply endpoint with valid and invalid inputs.""" # Valid input test response = self.client.get('/add_multiply?a=2&b=3&c=4') self.assertEqual(response.status_code, 200) self.assertEqual(response.get_json(), {'result': 20.0}) # (2 + 3) * 4 = 20 # Invalid input test (non-numeric value for 'a') response = self.client.get('/add_multiply?a=foo&b=3&c=4') self.assertEqual(response.status_code, 400) self.assertEqual(response.get_json(), {'error': 'Invalid input'}) # Missing parameter test (missing 'c') response = self.client.get('/add_multiply?a=2&b=3') self.assertEqual(response.status_code, 400) self.assertEqual(response.get_json(), {'error': 'Invalid input'}) def test_calculate_route(self): """Test the /calculate route for form submissions.""" # Test addition via form submission response = self.client.post('/calculate', data={ 'operation': 'add', 'a': '5', 'b': '7' }) self.assertIn(b'The result is: 12.0', response.data) # Test multiplication via form submission response = self.client.post('/calculate', data={ 'operation': 'multiply', 'a': '4', 'b': '6' }) self.assertIn(b'The result is: 24.0', response.data) # Test add and multiply via form submission response = self.client.post('/calculate', data={ 'operation': 'add_multiply', 'a': '2', 'b': '3', 'c': '5' }) self.assertIn(b'The result is: 25.0', response.data) # Test invalid input via form submission response = self.client.post('/calculate', data={ 'operation': 'add', 'a': 'foo', 'b': 'bar' }) self.assertIn(b'Error: Invalid input provided.', response.data)if __name__ == '__main__': unittest.main() And you chould obtain the following result:

Ran 4 tests in 0.023sOK So, here’s what this code does

  • Validates end-to-end functionality: It ensures that the application handles requests properly from the client’s perspective, including processing inputs, performing computations, and returning appropriate responses for each endpoint.
  • Tests interactions between components: Verifies that the individual functions (add_numbers, multiply_numbers) integrate correctly with the Flask routes and handle data as expected.
  • Checks input validation and handles errors: Confirms that the application responds correctly to invalid inputs or missing data, returning appropriate error messages and HTTP status codes.
  • Simulates real user behavior: Uses the Flask test client to mimic real-world interactions without running the actual server, facilitating efficient and automated testing.

Note that, with this code, you tested all the endpoints and their expected functionalities from a user’s perspective. The add_multiplyendpoint is particularly to be mentioned because it does not perform its logic independently from the others two, so it couldn’t be tested with only unit tests (if you’d write a unit test for it, you’d end up testing individual components – add_numbers() or multiply_numbers() – in isolation.)

ConclusionsIn this article, we presented the basics of test automation, discussing both unit testing and integration testing approaches.

While the theory is important, understanding these concepts requires practical experience and time. For this reason, the step-by-step guide provided offers a hands-on approach to learning about test automation using Python and Flask. But this is only the first step towards mastering this topic, so keep practicing and exploring the various techniques to become proficient in test automation!

Also, for improving even more your experience, you find all the code in this public repository. When you clone it, consider that you can do even more. In fact, to improve your automated testing skills and experience even more, I have created a CI using Semaphore CI: this firse up your automation journey!

Happy coding!

The post Unit Testing vs. Integration Testing: Test Automation Basics appeared first on Semaphore.

View Details

When it comes to testing, Test-Driven Development (TDD) and Behavior-Driven Development (BDD) are two of the most widely used methodologies. While the terms “TDD” and “BDD” are often used interchangeably, they represent distinct approaches involving different goals and stakeholders. It is important to understand the key differences between them to avoid confusion.

TDD focuses on complete coverage of code functionality by writing tests before the actual code implementation. In contrast, BDD is about capturing and validating business requirements through tests written in natural language, making them accessible and understandable to all stakeholders.

In this TDD vs BDD article, you will learn what Test-Driven Development and Behavior-Driven Development are, how they work, when to use each, and whether they can be combined effectively.

Let’s dive in!

What Is TDD (Test-Driven Development)?TDD, short for Test-Driven Development, is a proven software development methodology where tests are written before the actual code. The technique follows a straightforward cycle:

  1. Write a test for a new feature
  2. Create the minimum code needed to pass the test
  3. Refactor the code while ensuring the test still passes

This approach promotes better design, enforcing code correctness, and leads to higher-quality software.

TDD is attributed to Kent Beck, who introduced it in the late 1990s as part of Extreme Programming—an Agile methodology aimed at improving software quality and the development process.

Over time, TDD gained widespread popularity, especially within Agile development. Today, it is a cornerstone of modern software engineering, particularly in environments that prioritize automated testing.

How to Apply TDDAt its core, Test-Driven Development (TDD) follows a repetitive cycle:

The cycle in the above diagram is often summarized as Red-Green-Refactor. This process begins with the essential initial step of creating a list of test cases. From this list, you need to select one test and apply the Red-Green-Refactor cycle to it. Once completed, move to the next test in the list.

Note: Prioritizing test cases in the right order is key to efficiently addressing the most critical points in the design.

The loop is repeated for all test cases until the desired functionality is fully implemented. By breaking development into these manageable steps, TDD guides iterative and controllable progress.

Now, let’s explore the three main steps of the cycle in detail: Red, Green, and Refactor.

Step #1: Red PhaseThe cycle begins by writing a test that defines the next bit of functionality. This phase is called “Red” since the test must fail, as the corresponding code does not exist yet.

Benefits:

  • Provides a clear, incremental development goal
  • Forces you to focus on requirements and expected behavior before implementation

Potential issues:

  • Writing meaningful tests without the corresponding code can be challenging
  • Poorly structured tests may lead to ambiguity or misdirection in the next phase

Step #2: Green PhaseThe objective of this phase is to write just enough code to make the test pass. That code does not have to be perfect or optimized—it just needs to work.

Benefits:

  • Encourages minimalism, avoiding over-engineering
  • Quickly validates functionality, giving confidence in the code

Potential issues:

  • Focusing solely on passing the test might result in rushed, unstructured code
  • Tests with unclear objectives may lead to flawed implementations

Step #3: Refactor PhaseAfter the test passes, you need to improve the structure and quality of both new and existing code without altering the functionality under test.

Benefits:

  • Leads clean, maintainable, and extensible code
  • Reduces technical debt by addressing redundancies and inefficiencies

Potential issues:

  • Developers may skip this phase, leading to messy, hard-to-maintain code
  • Poor refactoring can introduce new issues if not carefully validated

TDD Tools and FrameworksHere is a list of some of the most popular and used TDD tools and frameworks:

  • Jest: A leading testing framework in the JavaScript ecosystem. Jest is designed for modern web applications and requires minimal configuration. It comes with built-in support for mocking, snapshot testing, and test coverage. See how to write unit tests using Jest in Node.js.
  • JUnit: A widely adopted framework for Java development. JUnit simplifies writing and running unit tests. It offers annotations for test methods and integrates seamlessly with IDEs and build tools like Maven and Gradle.
  • pytest: A versatile and feature-packed testing framework for Python. pytest excels in supporting fixtures, parameterized tests, and plugins. Learn more in our guide on how to test Python applications with pytest.
  • NUnit: A popular testing technology within the .NET ecosystem. NUnit provides a robust framework for writing and executing unit tests. It supports parameterized tests, setup/teardown methods, and a rich assertion library for TDD in .NET projects.

Test-Driven Development ExampleNow that you understand what TDD is, how it works, and the tools you can use to implement it, it is time to see a complete Test-Driven Development example.

Imagine you want to implement a feature to check whether a string is a palindrome. We will use Jest as the testing framework, but any other TDD tool will work.

This is the textual description of the functionality to implement:

“Check whether a given word or phrase is the same when read backward as it is forward (i.e., it is a palindrome).”

In the Red phase, you can write the following high-level test cases to verify both possible outcomes:

import { isPalindrome } from "utils.js"describe("isPalindrome", () => { it("should return true for a palindrome string", () => { // Arrange const input = "radar" const expectedOutput = true // Act const result = isPalindrome(input) // Assert expect(result).toBe(expectedOutput) }); it("should return false for a non-palindrome string", () => { // Arrange const input = "hello" const expectedOutput = false // Act const result = isPalindrome(input) // Assert expect(result).toBe(expectedOutput) })}) Keep in mind that the Red phase of TDD can involve writing multiple failing tests, especially when dealing with simple functionality that covers only a few possible scenarios. At this stage, running the tests will fail because the isPalindrome() function does not exist yet.

Note: These tests follow the AAA (Arrange, Act, Assert) pattern.

Next, in the Green phase, you have implement the isPalindrome() function to make the tests pass:

// utils.jsexport function isPalindrome(inputStr) { return inputStr === inputStr.split("").reverse().join("")} With this implementation, running the test case implemented earlier will now pass.

Finally, complete the Refactor phase to make the code capable of handling edge cases and more robust:

function isPalindrome(inputStr) { // to handle empty strings if (!inputStr) { return true } // normalize the input string to make the function // more robust const normalizedStr = inputStr.toLowerCase() return normalizedStr === normalizedStr.split("").reverse().join("")} Congratulations! You just used TDD to successfully implement the isPalindrome() function matching the desired functionality.

Get ready to dive into Behavior-Driven Development and compare it with TDD in this TDD vs BDD discussion!

What Is BDD (Behavior-Driven Development)?BDD, short for Behavior-Driven Development, is a collaborative software development approach that extends Test-Driven Development (TDD) and Acceptance Test-Driven Development (ATDD) by emphasizing communication and shared understanding among all team members.

BDD enhances TDD and ATDD with the following principles:

  • Apply the “five whys” to each proposed user story, ensuring its purpose aligns with business outcomes. If you are not familiar with that term, a “user story” is an informal, natural language description that outlines the features, functionality, or requirements of a software application from the perspective of the end user.
  • Think “from the outside in,” focusing on implementing only behaviors that directly contribute to these business outcomes.
  • Use a simple, unified language for describing behaviors, accessible to domain experts, testers, and developers, to improve communication.
  • Apply these practices throughout the software’s abstraction layers, paying attention to behavior distribution, making future changes easier and more cost-effective.

In simpler terms, Behavior-Driven Development defines the expected behavior of a software system using scenarios in natural language. This way, both technical and non-technical stakeholders can understand.

As explained by Dan North in his article “Introducing BDD” published in 2006, Behavior-Driven Development tries to address the challenges of TDD in writing effective tests. The ultimate goal is to bridge the gap between technical testing and business needs by framing tests as examples of desired behavior.

How to Perform BDDImplementing BDD revolves around writing behavior examples and then developing automated tests to verify that the software behaves as expected. This process usually involves three steps:

  1. Discovery: Understand the requirements
  2. Formulation: Define the acceptance criteria
  3. Automation: Turn acceptance criteria into automated tests

Let’s explore those steps in detail!

Step#1: DiscoveryIn this phase, the team collaborates to turn business requirements into user stories. A user story provides a clear, concise description of the behavior to be implemented. For example:

“As a user, I want to be able to log in to my account so that I can access my dashboard.”

The goal is to ensure that everyone on the team understands the feature’s purpose and the value it delivers to users. After defining the user story, developers, testers, designers, and managers work together to also create initial acceptance criteria. These specify potential scenarios and edge cases to clarify expectations.

Step #2: Formulation PhaseDuring this phase, broad acceptance criteria established earlier are refined into specific scenarios. Initially, acceptance criteria may be vague or general. For example, the discovery phase might provide the following high-level acceptance criterion for the login user story:

“A user should be able to log in to their account with valid credentials.”

The purpose of this phase is to translate them into clear and concrete examples of the expected system behavior. That eliminates misunderstandings between business and technical teams.

To achieve the result, user stories are transformed into structured BDD scenarios—generally expressed through the “Given-When-Then” format:

  • Given: Describe the initial state or pre-condition of the system before the behavior occurs.
  • When: Describe the action or behavior being performed.
  • Then: Describe the expected result or outcome of the behavior.

During the formulation phase, the simple criterion presented above can refined into a more detailed BDD scenario:

Scenario: Successful login Given the user is on the login page When the user enters valid credentials Then they should be redirected to their dashboard This scenario is written using Gherkin syntax, which is commonly supported by tools like Cucumber. This format ensures that all team members have a shared understanding of the feature’s behavior.

Step #3: Automation PhaseThe BDD scenario from the previous phase is automated through an acceptance test built using a BDD framework. Initially, the automated test will fail because the feature has not been implemented yet. However—as development progresses—and the code of the feature is implemented, the test will pass, validating that the feature works as intended.

Behavior-Driven Development ExampleConsider a Behavior-Driven Development (BDD) example to better understand how this methodology works.

Suppose the discovery phase lead to the following user story:

“As a user, I want to be able to sum two numbers”

That can be translated into a specific BDD scenario:

Scenario: Given two numbers, 5 and 3 When they are summed Then the result should be 8 Below is an acceptance test implemented with Jest, which also supports a BDD-like syntax:

import { sum } from "mathUtils.js"describe("Calculator", () => { it("should add two numbers correctly", () => { // Given const num1 = 5 const num2 = 3 // When const result = num1 + num2 // Then expect(result).toBe(8) })}) This automated test implements the Given-When-Then specifications. In particular, the describe() and it() methods are in line with in line with BDD principles as they allow you to structure tests in a human-readable format.

BDD ToolsBelow is a list of some of the most widely used BDD frameworks and tools:

  • Cucumber: A BDD framework that supports writing tests in Gherkin syntax, enabling collaboration between business and technical teams. Find out more in our article on writing acceptance tests with Cucumber.
  • Behave: A Python-based BDD framework that uses Gherkin syntax to define test cases, making it easy to validate application behavior against requirements.
  • SpecFlow: A .NET BDD framework that supports Gherkin syntax and integrates well with testing tools like NUnit, MSTest, and xUnit for behavior validation.
  • Behat: A PHP-based BDD framework, using Gherkin to describe application behavior in readable language, useful for functional testing.
  • Ginkgo: A BDD-style testing framework for Go, supporting expressive tests written in a behavior-driven manner.

Can BDD and TDD Coexist?TL:DR: Yes, BDD and TDD can coexist and should be used together for maximum synergy.

To better understand the role of TDD and BDD in a software development cycle, take a look at the diagram below:

As shown in the image, TDD focuses on verifying that individual components function correctly through several iterations of the Red-Green-Refactor loop (the inner cycle on the right). On the other hand, BDD loop (the broader outer cycle) emphasizes collaboration and aligns the system’s behavior with user expectations.

When used together, BDD shapes the development process by guiding it to meet user stories and business goals, while TDD enforces that the code is well-tested and reliable. The two methodologies serve complementary roles and work in tandem. It is no surprise that many testing tools accommodate both the TDD methodology and the creation of automated tests through BDD-like syntax, just like Jest.

TDD addresses technical correctness, while BDD focuses on aligning code with user behavior. Together, they create a feedback loop that continuously improves both code quality and user satisfaction.

TDD vs BDD: Summary TableExplore the differences between Test-Driven Development and Behavior-Driven Development in the TDD vs BDD comparison table below:

| Aspect | Test-Driven Development | Behavior-Driven Development | | --- | --- | --- | | Acronym | TDD | BDD | | Creators | Kent Beck and others | Dan North and others | | Creation time | Late 1990s | Early 2000s | | Inspirations | Extreme Programming | Test-Driven Development (TDD) and Acceptance Test-Driven Development (ATDD) | | Goal | Verify code correctness at a granular level | Verify the system’s behavior aligns with user requirements and expectations | | Phases | – Red (Write failing test)– Green (Make test pass)– Refactor (Improve code) | – Discovery (Understand requirements)– Formulation (Write scenarios)– Automation (Write automated tests) | | Focus | Ensure that individual units of code work correctly | Ensure that the software behaves as expected from the user’s perspective | | Approach | Developer-centric | Collaborative | | Team | Developers, QA engineers, and other technical roles | Developers, QA engineers, testers, and non-technical stakeholders |

ConclusionIn this BDD vs TDD comparison guide, you learned about the differences between Behavior-Driven Development and Test-Driven Development. While BDD and TDD are often mistaken for one another, they represent two distinct methodologies—each with its unique characteristics and tools.

The post TDD vs. BDD: What’s the Difference? (Complete Comparison) appeared first on Semaphore.

View Details

Continuous Integration (CI) is a foundational practice in modern software development. It plays a crucial role in agile workflows as it promotes collaboration, faster release updates, and keeps software aligned with business needs. It encourages developers to integrate code into a shared repository frequently.

CI pipeline steps like builds and tests automatically verify each commit. This way, developers can catch errors early, and improve code quality.

In this article, we’ll go through ten essential rules to maximize your CI pipeline’s efficiency and reliability. These rules will help you optimize build speed, improve testing accuracy, and boost overall development processes. You can apply these best practices to new or existing pipelines.

Top 10 Rules to Maximize Your CI PipelineLet’s dive into each rule and see how it can help enhance your CI pipeline.

  1. Use Version Control for a Shared Core RepositoryA reliable CI pipeline starts with a strong foundation: a version-controlled core repository. Version control (like Git) helps teams maintain a single source of truth, ensuring all code changes are traceable and easy to review. Version control enables smooth collaboration by allowing developers to work on branches, create pull requests, and conduct code reviews before merging changes.

In a CI setup, having all code in a central repository lets CI tools automatically pull the latest code for builds and tests. This centralization reduces conflicts, keeps everyone on the same page, and makes it easy to roll back changes if needed. Popular places to store your codebase are GitHub, GitLab, and Bitbucket.

  1. Commit Code FrequentlyFrequent commits are essential for maintaining a healthy CI pipeline. Committing small, incremental changes allows developers to catch issues early and keep code reviews manageable. If a problem occurs, it is easier to roll back to a previous commit.

Large, infrequent commits increase the chances of conflicts and make it harder to pinpoint the cause of errors. Moreover, infrequent commits can lead to a significant risk of losing work. If your changes are only stored locally and your machine fails—whether due to hardware issues or accidental data loss—you could lose hours or even days of effort.

Therefore, developers working on feature branches should push their code at least once daily.

Pushing your code daily isn’t about having everything perfect—it’s about progress and teamwork. Even if your code isn’t finished, sharing it regularly lets the team see what you’re working on, sparks helpful discussions, and ensures you make the right decisions on time. Frequent commits foster transparency, collaboration, and trust within the team.

  1. Automate BuildsAutomated builds are at the core of any CI pipeline. By setting up a CI tool to automatically build the application with each commit, you eliminate the need for manual builds and reduce the chance of human error. This is also a cost-efficient solution as you don’t need to hire experts solely to manage builds. Developers can focus on writing code instead of managing build processes. For example, platforms like SemaphoreCI offer a solution to set up your CI configuration.

Automated builds ensure new changes integrate smoothly with the existing code and verify that dependencies are installed correctly. Any broken build requires immediate attention. In a well-configured CI pipeline, the build step should fail if the build process encounters an issue. If someone pushes code that breaks the build, the CI pipeline will fail the build step. It will prevent the faulty code from being merged into the main branch, protecting the work of other team members who depend on a stable codebase. When a build is successful, it qualifies the application for further testing.

Build tools like Maven or Gradle handle tasks such as dependency management, compiling code, and packaging applications, making builds faster and more reliable. After a successful run, they typically generate artifacts, which are the outputs of the build process (e.g., JAR files, WAR files, or Docker images).

By integrating these tools into your CI pipeline, you ensure that builds are consistent across different environments.

  1. Automate Testing at Every LevelTesting is vital in CI. The most common types of testing include unit, integration, and end-to-end testing.

  2. Unit tests focus on testing individual functions or components in isolation. These tests should be fast to avoid bottlenecking the CI process. Each unit test should execute within milliseconds, ensuring it remains small, fast, and focused on testing a single functionality. While individual tests are quick, the full test suite might take several seconds to complete, depending on its size. This speed ensures rapid feedback during development.

Popular open-source tools for unit testing include JUnit (Java), Pytest (Python), and Jest (JavaScript).

  • Integration tests validate that different modules or components work together correctly. They are slower than unit tests, typically taking a few seconds per test, because they require setting up dependencies like databases, APIs, or external services. Integration tests help ensure that communication between components is seamless.

Tools like TestContainers (for testing with real dependencies), Spring Boot Test (Java), and Mocha (JavaScript) are popular for integration testing.

  • End-to-End (E2E) tests simulate user workflows by testing the entire application from start to finish. They ensure that all parts of the system work together as expected. E2E tests are significantly slower, typically running on the order of minutes per test, as they involve the complete system, including the user interface, backend services, and external integrations. While essential for catching system-wide issues, E2E tests should focus on critical paths to avoid bloating execution time in the CI pipeline.

Commonly used open-source tools for E2E testing include Cypress, Playwright, and Selenium.

Together, these tests form the foundation of the testing pyramid, a concept introduced by Mike Cohn, consisting of Unit Tests, Service Tests, and User Interface Tests.

Although it is useful, nowadays experts consider it too simplistic and potentially misleading. However, you can still implement some valid key points to design more efficient test suites:

  • Focus on Test Granularity: Write a higher number of small, fast unit tests, some coarse-grained integration tests, and a few high-level end-to-end tests. This ensures a balanced, maintainable, and efficient test suite.
  • Pyramid Shape: Stick to the pyramid’s structure to avoid creating the ATICC antipattern (Automated Testing Ice Cream Cone). It occurs when testing efforts focus too heavily on manual and high-level tests (e.g., UI or end-to-end), neglecting automated unit tests. This imbalance slows development, increases the risk of undetected bugs, and makes it harder to respond quickly in agile environments. To avoid this, prioritize automating unit tests while maintaining a balanced testing pyramid.

In a CI pipeline, unit tests act as the first line of defense, catching issues early before the code progresses further down the pipeline. If unit tests fail, it’s pointless to proceed with higher-level tests like integration or end-to-end tests. For example, Semaphore offers a fail-fast feature that can stop all remaining jobs when a job fails.

Another useful testing approach is static code analysis. For example, consider integrating lint into your CI. It automatically checks the source code for syntax errors, stylistic issues, and potential bugs. It helps ensure the code adheres to predefined coding standards and best practices, improving code quality and readability. You can use linting tools like ESLint for JavaScript, Pylint for Python, or Checkstyle for Java to automate this process.

You can also enable autolinting directly in your IDE, such as IntelliJ IDEA and VS Code. You can configure plugins like ESLint to automatically fix issues upon saving by adjusting the IDE’s settings. This ensures code quality as you type.

  1. Implement Smart Triggers and Branch-Specific PipelinesEfficient CI pipelines use smart triggers and branch-specific pipelines to optimize resource usage. Instead of executing the entire pipeline on every branch with each commit, you can set up path-based or branch-based triggers to control what jobs to run. For example, commits to the main branch might trigger full builds, while feature branches could run a smaller set of tests to save resources.

As mentioned earlier, unit tests are the foundation of any testing process and should be prioritized, while integration tests require more time and resources. Semaphore’s conditions provide the flexibility to define triggers based on regular expressions.

  1. Automate Code and Security ChecksIncorporating automated code quality and security checks into your CI pipeline helps ensure that your code remains maintainable and secure. Tools like SonarQube can automatically analyze your codebase for quality issues, such as bugs, code smells, and technical debt.

Additionally, security scanning tools like Trivy or Snyk can catch vulnerabilities in both your code and any dependencies you’re using. These tools automatically and continuously scan for known vulnerabilities, alert the team with findings and can even block branches with security flaws from being merged into the main branch.

  1. Enable Notifications and Feedback LoopsInstant feedback is crucial for maintaining a smooth development process. By enabling notifications for build or test failures, teams can quickly address issues as they arise. For example, sending Slack or email alerts helps ensure that developers know immediately when something goes wrong, reducing downtime and frustration.

In addition to notifications, automated reporting tools like SonarQube can provide detailed insights into code quality, test coverage, and other metrics.

Semaphore also offers a test reports dashboard. Semaphore’s Test Reports highlight critical failures and provide detailed insights into your test suite. You can filter tests and sort them by failure order or slowness, making it easier to prioritize the most pressing issues. When a critical failure occurs, teams can treat it as a “stop the line” event. Everyone drops other tasks to collaborate and resolve the problem immediately. This approach keeps the CI pipeline healthy and ensures reliable software delivery.

These feedback loops help teams improve code quality over time and prevent unexpected problems.

  1. Mirror Production Environments for TestingTo ensure reliable and consistent testing, it’s essential to mirror production environments as closely as possible. Using tools like Docker to containerize applications helps create isolated, reproducible environments for testing.

Additionally, tools like TestContainers can be used to spin up necessary dependencies (e.g., databases, message brokers) in containers during tests. Running tests in containers or using TestContainers helps verify that the code behaves as expected in real-world conditions.

If you deploy your application to Kubernetes in production, it’s important to mirror that environment in CI for integration tests. Tools like Minikube or Kind allow you to run local Kubernetes clusters within your CI pipeline to test containerized applications in an environment that mimics production.

However, be careful to avoid heavy end-to-end tests, resulting in slow feedback and inefficiency. Instead, reserve production-like environments for the most critical integration test paths and ensure earlier pipeline stages cover comprehensive unit and functional testing.

  1. Monitor and Measure CI PerformanceContinuous monitoring and measuring the performance of your CI pipeline is crucial for identifying bottlenecks and optimizing the workflow. Regularly monitoring CI performance allows teams to proactively address issues, rather than waiting for problems to arise.

Using tools like Semaphore CI Project Insights, you can track key metrics such as build times, test durations, and success rates. These insights help you identify areas for improvement, such as slow-running tests or inefficient build steps.

Semaphore also allows you to detect flaky tests. These are tests that are unreliable and fail randomly for no obvious reason. The flaky test detection works automatically once you set up test reports.

You can also conduct developer surveys to find areas for improvement. For example, you can identify pain points in the workflow, such as excessive waiting times, complex setup processes, lack of visibility, etc.

A good rule of thumb for optimizing CI pipelines is to keep build stages under 10 minutes to avoid developer frustration and provide time for breaks. Similarly, aim to keep test execution times under 5 minutes for fast feedback.

By addressing these issues, you’ll improve the overall efficiency and developer experience.

  1. Optimize Build and Test PerformanceEfficiency in your CI pipeline is essential for saving time and resources.

One way to optimize build and test performance is by implementing dependency caching. Caching dependencies that don’t change frequently can significantly speed up builds, as your CI system doesn’t have to download or compile them every time. For example, Semaphore provides a caching mechanism to help speed up your builds.

Additionally, parallelizing tests allows you to run multiple tests simultaneously, reducing overall test execution time. You can use techniques like Semaphore’s job parallelism to speed up your test suites.

For complex systems, mocking external services or environments that aren’t essential to the test can further speed up execution by eliminating unnecessary dependencies. Tools like WireMock, MockServer, and Moco can simulate external APIs or services, reducing the need for actual network calls during testing. This allows you to isolate tests and ensure they run faster and more reliably.

ConclusionIn this article, you’ve learned the top 10 best practices to get the most out of your CI pipeline. To summarize, here’s a visual representation of a complete CI pipeline, incorporating the best practices we’ve discussed:

Following these rules will help improve the overall development cycle, reduce costs, and accelerate software delivery.

Thank you for reading!

The post Top 10 Rules of Continuous Integration appeared first on Semaphore.

View Details

Web applications are increasingly transforming browsers into the primary interface for our digital lives. As we rely more on web applications for everything from work to entertainment, the responsiveness of these applications becomes paramount. Rich Harris, a former journalist and the mind behind Svelte, shares his insights into web development technologies and trends.

Edited transcriptionA journalist by trade, Rich Harris recognized the potential of web technologies to enable “new forms of interesting and exciting journalism that involve visualizing large datasets.” Curious about how these technologies could help him do a better job, he began learning JavaScript and eventually open-source software development: “I began building tools for myself and then sort of sharing them on GitHub, discovering that other people had the same needs,” he reflects. Ultimately, Rich kept diving deeper into software development and created Svelte and its companion framework, SvelteKit, and transitioned out of journalism when he secured sponsorship from Vercel for his project.

Svelte is a JavaScript framework for building user interfaces. Unlike traditional frameworks like React and Vue, Svelte compiles components into highly optimized JavaScript code, resulting in faster performance from the client and smaller bundle sizes. Rich describes Svelte as “a way of building interactive websites with a lot more reliability and a lot less pain than if you were manually wiring up all of the DOM manipulation code yourself.”

Building the frameworkThe most defining philosophical difference between Svelte and other frameworks is the prominence of the compiler. As Rich puts it, “It doesn’t make sense to do a lot of work in the browser when every single user has to do that work and you have to ship the code that does that work when you could just do that at compile time.”

The compiler allows writing code “in this sort of declarative form that very closely resembles HTML, but with interactivity added on top of it,” Rich explains. Hence, unlike JavaScript-centric frameworks, Svelte allows developers to use JS, HTML, and CSS in a more integrated manner. “When you build a component in Svelte, you can include styles,” Rich notes. This scoped CSS approach ensures that styles defined within a component are isolated to that component. “It makes sense if that CSS belongs with the component in such a way that the framework can keep the styles defined with the components scoped to that component, which is something that Svelte does.”

In this regard, Rich believes other frameworks often “lean more in the direction of letting the community solve” issues. While he acknowledges that this can produce excellent solutions—since “people can really focus very intensely on one specific problem”—it also places a burden on developers. “Everyone who uses the framework needs to spend time thinking about which of the many solutions on display they’re going to use, and which is right for their application,” he says. In contrast, Svelte puts developer experience first: Users “want to be able to start writing code and be productive, and have the answer ready to the question, ‘How do I do this thing?’ before they start working.”

Many of Svelte’s features were shaped by this principle, including built-in support for animations and transitions as first-class primitives. “Whereas in other frameworks, you might need to research which library to use and then figure out how to integrate it, Svelte just supports these features out of the box,” says Rich. Likewise, Svelte’s implementation of signals introduced a more flexible way to manage state and dependencies. Signals are a mechanism for building reactive systems that automatically update the UI whenever their values change: “We’re able to be extremely surgical about which parts of the page update in response to a given change,” he says.

Finding the balance between server and client workloadWhile Svelte has been optimized to prioritize build-time tasks, there are inherent limits to what can be achieved with it. As Rich confesses, “When you’re building a large enough application, you will find that there are things that you just cannot know at build time; you need to have the information of the dependency graph available at runtime, and that just wasn’t there.

Shifting workloads between the browser and the server is not a settled matter; in fact, it has become a recurring trend in web development, fed by hype cycles. “We shouldn’t be surprised that we’re in the middle of another one of these pendulum swings right now,” says Rich. To his understanding, these cycles often overcorrect previous trends. For example, during the 2010s, developers increasingly shifted workloads to the browser following the Ajax revolution sparked by Gmail, which led to the proliferation of large JavaScript bundles that often overloaded the browser. In turn, over the past few years, there’s been a shift back to server-side rendering, a trend of which Svelte is a part of.

Withal, Rich advocates for a balanced approach. “Code should run where it’s needed. If you have a supercomputer in your pocket and the ability to render a high-fidelity interactive user experience using code that runs on that device, then do it there.” Web applications can learn from native apps, which operate offline; excessive reliance on servers undermines offline functionality—a feature many users value.

To balance the workload, Svelte has SvelteKit, a framework built on top of Svelte for creating modern web applications. For enabling offline functionality, SvelteKit supports Progressive Web App (PWA) features. For example, SvelteKit can be configured to use service workers—scripts that run in the background to cache essential assets like HTML, CSS, and JavaScript files. When users revisit the application, these cached assets allow the app to load instantly, even without a server connection. SvelteKit also supports pre-rendering pages at build time, generating static HTML for specific routes or pages. These pre-rendered pages can be cached on the user’s device, ensuring they remain accessible offline. Additionally, SvelteKit facilitates API requests and client-side data fetching. With caching strategies, developers can store API responses and other data offline using technologies like service workers or IndexedDB.

The bottom lineTo get involved with Svelte, Rich encourages developers to create component libraries, which are essential building blocks for applications. Visit github.com/sveltejs to learn more. Rich also invites enthusiasts to join the Svelte community on Discord at svelte.dev/chat.

You can follow Rich in X at @rich_harris.

The post Rich Harris on the Future —and Past— of Web Development appeared first on Semaphore.

View Details

Security is a critical concern in modern software development, especially when dealing with open-source dependencies. Vulnerabilities in these libraries can introduce severe risks to your application, exposing it to potential attacks. That’s why it is essential to ensure that your codebase remains secure.

Snyk, a powerful open-source security tool, helps you to identify and fix vulnerabilities in your codebase. When you combine it with Semaphore CI’s CI/CD pipeline, you can automate these checks and ensure that your software stays secure at every step of the development process.

In this article, you will learn:

  • How Snyk works, with a focus on Snyk Open Source.
  • How to integrate Snyk with Semaphore CI for automated vulnerability scanning.
  • Best practices for securing your CI/CD pipeline.

Let’s get started!

Snyk OverviewWhat is Snyk?

Snyk is a comprehensive platform designed to secure every aspect of your application stack, from code to infrastructure. It provides a suite of tools to identify and remediate vulnerabilities in your codebase, including:

  • Snyk Code: Detects vulnerabilities in your source code.
  • Snyk Container: Secures container images against vulnerabilities.
  • Snyk Infrastructure as Code (IaC): Fixes misconfigurations in IaC tools like Terraform and Kubernetes.
  • Snyk Open Source: Scans and resolves vulnerabilities in your open-source dependencies.

One of Snyk’s standout features is its deep integration with CI/CD pipelines. It allows you to automatically run security scans.

Snyk offers a free tier, making it easy for individual developers and small teams to get started.

In this article, we’ll focus on Snyk Open Source. It automatically identifies and fixes vulnerabilities in open-source libraries and dependencies. It scans your project’s libraries, flags security risks, and offers advice to remediate the vulnerabilities.

Snyk Open Source scans your project’s dependencies for vulnerabilities using its continuously updated vulnerability database. This database pulls from various sources, including the National Vulnerability Database (NVD), security advisories, GitHub issues, and Snyk’s in-house security research team.

How to Use Snyk?

You can utilize Snyk in several ways:

  • Web UI: Manage, configure, and fix issues through Snyk’s web interface.
  • CLI: Integrate Snyk into your local machine or CI/CD pipelines (which you will see in this article).
  • IDEs: Use Snyk within your development environment.
  • API: Programmatically integrate Snyk into your workflows (available only for Enterprise plans).

Setting Up SnykFollow these steps to get started with Snyk.

  1. Create a free Snyk Account by visiting the Snyk website.
  2. To run security checks locally, install the Snyk CLI:

curl --compressed https://downloads.snyk.io/cli/stable/snyk-linux -o snykchmod +x ./snyksudo mv ./snyk /usr/local/bin/ Check the installation guide for more installation options.

  1. After installation, authenticate Snyk CLI with your account by running:

snyk auth This command will open a web browser where you can log in to your Snyk account. Finally, you should see the following messages in your Terminal:

Opening in existing browser session.Your account has been authenticated. 4. Navigate to your project’s directory and run the following command to scan for vulnerabilities:

snyk test Snyk will analyze your project and provide a report of any existing vulnerabilities. For example:

Tested 5 dependencies for known issues, found 5 issues, 5 vulnerable paths.Issues to fix by upgrading: Upgrade ch.qos.logback:logback-classic@1.2.3 to ch.qos.logback:logback-classic@1.2.13 to fix ✗ Insufficient Hostname Verification [Medium Severity][https://security.snyk.io/vuln/SNYK-JAVA-CHQOSLOGBACK-1726923] in ch.qos.logback:logback-core@1.2.3 introduced by ch.qos.logback:logback-classic@1.2.3 > ch.qos.logback:logback-core@1.2.3 ✗ Denial of Service (DoS) [High Severity][https://security.snyk.io/vuln/SNYK-JAVA-CHQOSLOGBACK-6094943] in ch.qos.logback:logback-core@1.2.3 introduced by ch.qos.logback:logback-classic@1.2.3 > ch.qos.logback:logback-core@1.2.3 ✗ Uncontrolled Resource Consumption ('Resource Exhaustion') [High Severity][https://security.snyk.io/vuln/SNYK-JAVA-CHQOSLOGBACK-6097493] in ch.qos.logback:logback-core@1.2.3 introduced by ch.qos.logback:logback-classic@1.2.3 > ch.qos.logback:logback-core@1.2.3 ✗ Denial of Service (DoS) [High Severity][https://security.snyk.io/vuln/SNYK-JAVA-CHQOSLOGBACK-6094942] in ch.qos.logback:logback-classic@1.2.3 introduced by ch.qos.logback:logback-classic@1.2.3 ✗ Uncontrolled Resource Consumption ('Resource Exhaustion') [High Severity][https://security.snyk.io/vuln/SNYK-JAVA-CHQOSLOGBACK-6097492] in ch.qos.logback:logback-classic@1.2.3 introduced by ch.qos.logback:logback-classic@1.2.3 Optionally, you can link your repository (like GitHub or Bitbucket) to allow Snyk to scan your project for vulnerabilities. However, we will skip this step as our focus will be on using Snyk within the CI/CD pipeline process.

Integrating Snyk with Semaphore CINow that you know how Snyk works, let’s integrate it into the Semaphore CI pipeline.

  1. First, you need a free Semaphore account. Visit the Semaphore website and choose either GitHub or Bitbucket for registration. In this tutorial, I will use GitHub.

  2. Add a new project by clicking the “+Create New” button.

I’ve created a simple Maven project with minimal source code and a single JUnit test. I will connect my repository with Semaphore:

  1. Click on “Continue to workflow setup”. Semaphore will automatically initialize the project. You can add more people to it if you wish.
  2. Next, we need to create the workflow for the pipeline. You can configure the Semaphore pipeline via the UI or manually create the semaphore.yml file in your project. I will show you the first approach.
  3. Select the “Maven” template from the list. Click on “Customize” to override the default configuration:

  4. Change the default machine type to e1-standard-2. The e1 series machines are appropriate for less compute and memory-intensive jobs.

  5. Edit the Build block to combine the build and test steps into a single block since they share the same initial commands. Instead of duplicating these commands for each job, we can use the Prologue section to streamline the process. Add the shared commands to the Prologue, ensuring they run once before the jobs.

The prologue commands:

checkoutsem-version java 17 This command checks out the project’s code. The sem-version utility on Linux-based virtual machines allows you to set or switch the version of a programming language.

The build job command:

mvn -B package --file pom.xml This command compiles the project’s source code and packages it using Maven, creating a distributable artifact.

The test job command:

mvn test This command executes the project’s unit tests using Maven.

  • Click “+ Add Block” to add a job for the security scan:

Security scan commands:

checkoutcurl --compressed https://downloads.snyk.io/cli/stable/snyk-linux -o snykchmod +x ./snyksudo mv ./snyk /usr/local/bin/SNYK_TOKEN=$SNYK_TOKEN snyk test These commands install the Snyk CLI tool, make it executable, place it in a system directory, and initiate a vulnerability test using an authentication token. We reference the $SNYK_TOKEN variable, which serves for authentication with Snyk. As a best practice, this token is stored as a secret to avoid exposing sensitive information. To create the secret, expand the “Secrets” section and open “Manage Secrets” in a new tab.

Select “Set up global secret” and name it “SNYK_TOKEN“. Input the token value, which you can retrieve from your Snyk account’s General Settings.

Click on “Save Secret”.

  1. Go back to the workflow. The new secret name should appear under the Secrets section. Select it:

  2. Click the “Run the workflow” button, and Semaphore will automatically start the pipeline. Additionally, it will create a new branch called “setup-semaphore” in your GitHub repository.

If you prefer the YAML creation approach, check out the link to my GitHub repository, which contains the content of the semaphore.yml file. Keep in mind that you still have to create the secret manually.

Testing the WorkflowThe pipeline should pass because we don’t have any vulnerabilities yet:

Let’s add an open-source library with known vulnerabilities to test the Security Scan job. Add this dependency to the pom.xml:

<dependency> <groupId>commons-beanutils</groupId> <artifactId>commons-beanutils</artifactId> <version>1.9.2</version> <!-- Known vulnerability in this version --></dependency> The pipeline should fail this time:

Best Practices for Secure CI/CD PipelinesTo maintain a secure CI/CD pipeline, consider these best practices for integrating security into your development workflow:

  • Run security scans on every pull request: Automate security scans to ensure every change is evaluated for vulnerabilities before merging.
  • Enforce policies against vulnerable code: Implement policies that prevent vulnerable code from being deployed.
  • Regularly update dependencies: Keep your dependencies up to date to minimize exposure to known vulnerabilities.
  • Store secrets as environment variables: Use environment variables or secret management tools to securely store sensitive information, like API tokens, and avoid hardcoding them in your pipeline configuration.
  • Break the build for critical issues: Set up your pipeline to fail if high-priority vulnerabilities are found, ensuring they are addressed before deployment.

ConclusionIncorporating Snyk into your Semaphore CI pipeline is a powerful step towards enhancing the security of your applications.

Throughout this article, you learned:

  • How Snyk, particularly Snyk Open Source, operates and what its key features are.
  • The step-by-step process for integrating Snyk with Semaphore CI for automated vulnerability scanning.
  • Best practices for securing your CI/CD pipeline, such as running security scans on every pull request and enforcing deployment policies against vulnerable code.

Thank you for reading!

The post Securing Your CI/CD Pipeline with Snyk Open Source and Semaphore appeared first on Semaphore.

View Details

You’re getting started with a new tool. You need to find your way around the documentation quickly. You’re not sure where to start reading. But imagine if someone could tell you which part of the documentation you should start reading first. So let’s build an AI that does that. Even if the documentation is partially written.

We will be building a bot that answers questions about the Semaphore documentation. You will learn how to build a RAG chatbot. And you will also build a tool that will help you get started with Semaphore.

But this will not be an ordinary RAG chatbot. It will also be useful while the documentation is still incomplete. So you will also learn a technique for working with sparse data.

What we are actually buildingWe are building a Python chatbot that will answer questions based on the new Semaphore documentation on GitHub. The bot will run in the terminal. It will answer questions about the new Sempahore documentation. It will provide a short answer and then direct you to the source document where you can read more.

These types of chat with documents systems are called Retrieval-Augmented Generation (RAG). When the user asks a question, the retriever searches for the relevant information. The retrieved information is inserted into the LLM’s prompt. And the LLM is instructed to generate the answer to the user’s question. The LLM’s answer must be based solely on the information provided in the prompt.

The LLM has a context window of a fixed size. The size of the input context window limits how much text we can include in the prompt. The size of the output context limits how much text the LLM can output. Therefore, we only augment the prompt with the most relevant chunks of documents. Because in most cases, we can’t fit all of our source documents into a single prompt.

In production, the source documents are stored in the system’s search database. In this tutorial, the documents will be stored in memory. Because we are building a first iteration quickly.

The code in this tutorial runs on Python 3.10.9, and the uses the following packages from pip.

langchain==0.3.0langchain-openai==0.2.0langchain-community==0.3.0faiss-cpu==1.8.0.post1 The LLM that we are using is gpt-4o-mini. But you could swap it for any LLM that you prefer to use.

Is the data usable? This is the greatest challengeThe greatest challenge is that the documentation is still being written. At the time of writing, the new documentation is very sparse. Some documents consist of three letters, “WIP”. Some documents are mostly example code. Some documents are mostly a diagram or two. You can imagine a presenter at a data conference bleating “Garbage-In Garbage-Out!” But it’s my job to deliver a solution despite the sparse data. So here’s the solution that I came up with.

We can often guess the purpose of a document before it has been completed. We look for clues. For example, a file called “SECURITY.md” will probably be about security. Even if the contents of that file are just three letters, “WIP”.

Let’s imagine another example. A file that contains a single YAML template, and no text to describe the template. This file is probably related to config.

We will be using the LLM to augment our documents with information that is implied by their path, file name, and content so far. Some documents have a lot of text already. Others have a few letters. To make every “document” is roughly the same size, we will use the LLM to generate summaries of those documents. Our RAG chatbot will only use the document summaries and the augmented context. The bot will explicitly direct the user to the source documents in each answer.

At the time of writing, the document preprocessing step takes approximately five minutes. And it costs less than $0.10 in OpenAI fees.

Anthropic have recently published an article that also describes a technique of augmenting data for RAG. In a typical RAG use case, the documents are split into chunks. When the user asks a question, the retriever retrieves only the most relevant chunks. Usually from several different source documents. In Anthropic’s Contextual Retrieval, they generate additional context for each chunk of text. This can help the retriever understand what the source document was talking about. In our case, we are not chunking our “documents”. For each source document, we are retrieving the whole summary plus augmented context.

For example, here is a “summary plus augmented context” for docs/getting-started/guided-tour.md.

The path 'docs/getting-started/guided-tour.md' suggests that this document serves as an introductory guide for new users of SemaphoreCI, helping them navigate the initial steps of setting up and using the tool effectively.This document provides a guided tour for new users of SemaphoreCI. It outlines essential tasks such as signing up, creating an organization, setting up a first project, and understanding Continuous Integration and Continuous Delivery processes. The document aims to help users get started with SemaphoreCI quickly and effectively, ensuring they can leverage the tool for their CI/CD workflows.The 'Guided Tour' document is a 20-minute tutorial designed to help new users of SemaphoreCI get acquainted with the platform. It emphasizes that no credit card is required to try out Semaphore, making it accessible for users to explore its features without any commitment. The tutorial covers four key learning objectives: first, it guides users through the sign-up process and organization creation; second, it introduces a simple 'Hello, World!' project to demonstrate the basic functionalities of CI; third, it provides a real-world example of Continuous Integration, including build and test steps; and finally, it explains the Continuous Delivery process, which includes releasing and deploying apps. Overall, this document serves as a foundational resource within the broader documentation, aimed at onboarding users efficiently and encouraging them to leverage SemaphoreCI for their development workflows. Another example is SECURITY.md. At the time of creating this tutorial, the file is a work in progress literally containing just three letters: “WIP”. On their own, these three letters would be useless to the retriever. Here is a “summary plus augmented context” for SECURITY.md:

The path 'SECURITY.md' suggests that this document is focused on security practices, guidelines, or considerations relevant to the Semaphore CI/CD tool. It likely outlines how users can secure their CI/CD processes, manage sensitive data, and comply with security standards.This document is intended to provide insights into the security protocols and best practices associated with using Semaphore CI/CD. Security is a critical aspect of CI/CD tools as they handle code, configurations, and potentially sensitive data. This document will help users understand how to implement security measures within their Semaphore workflows, contributing to overall operational security.The document labeled 'Security' is currently marked as 'WIP' (Work In Progress), indicating that it is not yet complete. While no specific content is provided, the intention behind the document is to outline security practices and considerations for using SemaphoreCI. Once fully developed, it is expected to cover topics such as securing deployment pipelines, managing access controls, ensuring the integrity of build artifacts, and maintaining compliance with security regulations. This document will be part of the broader SemaphoreCI documentation suite, which addresses various functionalities such as Continuous Integration, Pipelines, and Automation, among others. It will be particularly useful for developers and operations teams looking to implement secure CI/CD practices. Our prompt for summarizing and augmenting the documentsHere’s the prompt template that we will be using to summarize and augment the source documents. Notice that I wrapped it in a Python multiline string. A prompt template is different to a finished prompt. A prompt template has space for us to insert our data. The strings {path} and {doc} will be replaced by data. They will hold the path to a source document and the text of the source document.

This Python string will live in a file called prompts.py. prompts.py contains three python strings. You will see the other two strings later in this tutorial.

```

This is the prompt for summarizing and augmenting the docssummarize_and_augment_prompt = """The document in the "document" XML tags is part of the documentation for a CI/CD tool called SemaphoreCI.The path in the "path_to_document" XML tags is path to that particular document. Pay attention to the file and folder names because they suggest the purpose of the document.Semaphore is a CI/CD solution to streamline developer workflows.Semaphore features Continuous Integration and Pipelines, Deployments and Automation, Metrics and Observability, Security and Compliance, a Developer Toolkit, Test Reports, Monorepos, and Self-hosted build agents.Semaphore works in the cloud, on-premises, and in the hybrid cloud.{path}{doc}The document in the "document" XML tags is part of the documentation for a CI/CD tool called Semaphore.The path in the "path_to_document" XML tags is path to that particular document. Pay attention to the file and folder names because they suggest the purpose of the document.Summarize the document. Add context to the document as well. You will need to explain what task it is useful for. And where this document fits into the broader documentation.Use the following JSON template.{{ "what_path_suggests": Look at the path to the document. What does the path suggest about this document's purpose? "what_is_the_document_about": What is this document about? How does it help us use SempahoreCI? "summarize_document": Summarize the document. If the document has a lot of text, then write many paragraphs. If the document has code or a template, describe what you think tha template is for. What language is the code or template in?}}"""

`` Building the retrieverPreprocessing the documentsThe new Semaphore documents are stored in a git repo. We will first clone the repo to our local disk. Then we will use Pythonos.path.walk()` to iterate through each document. For each document, we will call the LLM to summarize and augment it. We will also generate an embedding for the combination of the summary and the augmented context.

The code below highlights the important details. The complete code file will be presented later in this article.

How to iterate through each document in the repo import os#1. Setup# Define the directory containing the markdown filesdocs_directory = "../semaphore/docs/"#3.0 Loop through and summarize the markdown files.for root, dirs, files in os.walk(docs_directory): #3.1 We loop through the markdown files only for filename in [ fi for fi in files if fi.endswith(".md") ]: file_path = os.path.join(root, filename) #3.2 Read the contents of the markdown file with open(file_path, "r") as file: content = file.read() #...Then we call the LLM for each document in a loop here. How to use the LLM to summarize and augment each documentWe are using the Lanchain library. Note that we are not using any of Langchain’s output parsers. We are using OpenAI’s JSON mode to get a structured output. Our chain returns a dictionary object. To enable JSON mode, we use model = ChatOpenAI(model=model_to_use).with_structured_output(None, method="json_mode").

We are also creating metadata for each document. We are creating a field called source. The source of a document is the url of its location on github. The chatbot will display the full URL when answering the user’s questions. The user will be able to click on the URL and view the document on github. The code below shows how it’s done.

import prompts #This is prompts.py, which I mentioned above.from langchain_openai import ChatOpenAIfrom langchain_core.prompts import ChatPromptTemplate#1. Setup# Define the directory containing the markdown filesclickable_url_prefix = "https://github.com/semaphoreci/semaphore/tree/main/docs/"docs_directory = "../semaphore/docs/"model_to_use = "gpt-4o-mini"# Check if output_dir is missing. mkdir if missing.if not os.path.exists(output_dir): os.makedirs(output_dir)summaries = []summary_metadata = []#2.0 Setting the Langchain objects##2.1 Setting up a Langchain "model" objectmodel = ChatOpenAI(model=model_to_use).with_structured_output(None, method="json_mode")##2.2 Setting up our prompt templateour_prompt_template = ChatPromptTemplate.from_messages( [ ("user", prompts.summarize_and_augment_prompt)])#3.0 Loop through and summarize the markdown files.for root, dirs, files in os.walk(docs_directory): #3.1 We loop through the markdown files only for filename in [ fi for fi in files if fi.endswith(".md") ]: file_path = os.path.join(root, filename) #3.2 Read the contents of the markdown file with open(file_path, "r") as file: content = file.read() #3.3 Use the LLM to summarize the markdown file our_path = file_path.replace(docs_directory,"") clickable_url = clickable_url_prefix + our_path chain = our_prompt_template | model json_formatted = chain.invoke({"path": our_path, "doc": content}) #3.4 Append to the summary and path to the lists the_summary = json_formatted["what_path_suggests"] the_summary += "\n" the_summary += json_formatted["what_is_the_document_about"] the_summary += "\n" the_summary += json_formatted["summarize_document"] the_summary += "\n" summaries.append(the_summary) summary_metadata.append({"source": clickable_url}) How to compute the embeddingsOur retriever searches based on embeddings rather than based on text. Next we need to compute embeddings based on the text that we have just generated. We will compute the “document” embeddings once and then save them to disk.

When the user asks our chatbot a question, we compute the embedding for the question. We then compare our embedded question with the embeddings of our “documents”. We retrieve the documents with the most similar embeddings. The exact implementation is called FAIS and we’ll be using a library for it.

At the end of the code snippet below, we set up a dictionary object that holds the text to be retrieved, the embeddings, and the metadata. In this case, the metadata is the path to the source document. We use the pickle library to save this dictionary object to disk.

import picklefrom langchain_openai import OpenAIEmbeddings#1. Setup# Define the directory containing the markdown filesclickable_url_prefix = "https://github.com/semaphoreci/semaphore/tree/main/docs/"docs_directory = "../semaphore/docs/"output_dir = "summarized_docs/"model_to_use = "gpt-4o-mini"output_pickle_file_name = "texts_and_embeddings.pkl"output_pickle_file_path = os.path.join(output_dir, output_pickle_file_name)#...Document looping/generation code has been omitted for clarity.#4.0 Generate the embeddingsprint("Embedding summaries...")summary_embeddings = embeddings.embed_documents(summaries)#5.0 zip everything together and save to disktext_embedding_metadata_triplets = {"embeddings":zip(summaries, summary_embeddings), "meta": summary_metadata}with open(output_pickle_file_path, "wb") as p_file: pickle.dump(text_embedding_metadata_triplets, p_file) The complete preprocessing fileWe store the code to preprocess the documents in a file called summarize_docs.py.

```

This file summarizes the documents, creates embeddings, and saves the data to a pickle file.import osimport pickleimport promptsfrom langchain_openai import ChatOpenAIfrom langchain_openai import OpenAIEmbeddingsfrom langchain_core.prompts import ChatPromptTemplate#1. Setup# Define the directory containing the markdown filesclickable_url_prefix = "https://github.com/semaphoreci/semaphore/tree/main/docs/"docs_directory = "../semaphore/docs/"output_dir = "summarized_docs/"model_to_use = "gpt-4o-mini"output_pickle_file_name = "texts_and_embeddings.pkl"output_pickle_file_path = os.path.join(output_dir, output_pickle_file_name)# Check if output_dir is missing. mkdir if missing.if not os.path.exists(output_dir): os.makedirs(output_dir)summaries = []summary_metadata = []# Optional: Prompt the user for the API key#import getpass#os.environ["OPENAI_API_KEY"] = getpass.getpass()#2.0 Setting the Langchain objects##2.1 Setting up a Langchain "model" objectmodel = ChatOpenAI(model=model_to_use).with_structured_output(None, method="json_mode")##2.2 Setting up our prompt templateour_prompt_template = ChatPromptTemplate.from_messages( [ ("user", prompts.summarize_and_augment_prompt)])#2.3 Set up the embeddings modelembeddings = OpenAIEmbeddings()#3.0 Loop through and summarize the markdown files.for root, dirs, files in os.walk(docs_directory): #3.1 We loop through the markdown files only for filename in [ fi for fi in files if fi.endswith(".md") ]: file_path = os.path.join(root, filename) #3.2 Read the contents of the markdown file with open(file_path, "r") as file: content = file.read() #3.3 Use the LLM to summarize the markdown file our_path = file_path.replace(docs_directory,"") clickable_url = clickable_url_prefix + our_path chain = our_prompt_template | model json_formatted = chain.invoke({"path": our_path, "doc": content}) #3.4 Append to the summary and path to the lists the_summary = json_formatted["what_path_suggests"] the_summary += "\n" the_summary += json_formatted["what_is_the_document_about"] the_summary += "\n" the_summary += json_formatted["summarize_document"] the_summary += "\n" summaries.append(the_summary) summary_metadata.append({"source": clickable_url}) #3.5 Optional: Write the sumarised file to disk s_file_name = os.path.join(output_dir, our_path.replace("/","_")) with open(s_file_name, "w") as s_file: print(s_file_name) path_spec_text = "Path to file: {0} \n\n".format(our_path) s_file.write(the_summary)#4.0 Generate the embeddingsprint("Embedding summaries...")summary_embeddings = embeddings.embed_documents(summaries)#5.0 zip everything together and save to disktext_embedding_metadata_triplets = {"embeddings":zip(summaries, summary_embeddings), "meta": summary_metadata}with open(output_pickle_file_path, "wb") as p_file: pickle.dump(text_embedding_metadata_triplets, p_file)

``` How to retrieve the relevant documents at query timeWe’re using FAIS as our retriever. I chose FAIS for this tutorial because it runs in memory. We don’t need to worry installing any databases. In production, you might decide to outsource the retriever to a cloud vendor’s search product.

The code below is an extract from a larger file that is called rag_bot.py. The complete file will be presented later in this article.

We read the pickled data from disk. Then we instantiate a retriever object. To test our retriever, we could search our documents with retriever.invoke(). For example, retriever.invoke("What do I need to keep in mind for security?")

import osimport picklefrom langchain_openai import OpenAIEmbeddingsfrom langchain_community.vectorstores import FAISS#1. Setupdocs_dir = "summarized_docs/"docs_pickle_file_name = "texts_and_embeddings.pkl"docs_pickle_file_path = os.path.join(docs_dir, docs_pickle_file_name)with open(docs_pickle_file_path, "rb") as p_file: text_embedding_metadata_triplets = pickle.load(p_file)#2.3 Set up the embeddings modelembeddings = OpenAIEmbeddings()#3.0 Setting up the retrieverfaiss_vectorstore = FAISS.from_embeddings(text_embedding_metadata_triplets["embeddings"], embeddings,text_embedding_metadata_triplets["meta"])retriever = faiss_vectorstore.as_retriever() Building the question-answering componentThe RAG question-answering promptThe question-answering component has a system prompt and a user prompt. The system prompt and the user prompt template are shown below. They are stored in prompts.py.

```

This is the prompt for the RAG rag_system_prompt = """You are helping me find my way through the Semaphore CI documentation. You answer my questions based on the context. And then you direct to the source of the document, where I can read more.You must answer based on the context only. Do not use your general knowledge.If you don't know the answer then reply with "I don't know".You are always very optimistic and encouraging."""rag_user_prompt = """The question is in the "question" XML tags.The context is in the "context" XML tags.The context is made up of documents. Each "document" is delimited by its own XML tag.Each document has a "source" property. You need to direct me to the source of the document. Because the source of the document will have more details.Answer the question based on the context. If you don't know the answer then reply with "I don't know".{context}{question}The question is in the "question" XML tags.The context is in the "context" XML tags.The context is made up of documents. Each "document" is delimited by its own XML tag.Each document has a "source" property. You need to direct me to the source of the document. Because the source of the document will have more details.Answer the question based on the context. If you don't know the answer then reply with "I don't know".Use the following JSON template for your reply. Write valid JSON.{{ "answer": Answer the question. Make sure that you explain where I can find more information - in the source of the relevant documents. "sources": [Output the sources of the documents that you used to answer the question into this array.]}}"""

``` How to use the LLM to answer the questionThe retriever retrieves the documents that are relevant to our question. Then we need to insert those documents into the LLM’s prompt. The inserted documents are called the context. The prompt instructs the LLM to answer the question, but only based on the context in the prompt.

We need to concatenate all of the retrieved documents into one string. And the final string must be in a style that matches our prompt. We will write a function called format_docs(). You can see it in the code below. Note how we are using the metadata as well.

import promptsfrom textwrap import dedentfrom langchain_openai import ChatOpenAIfrom langchain_core.prompts import ChatPromptTemplatefrom langchain_core.runnables import RunnablePassthrough#0. Function used to arrange the search results. Could be used in other code.def format_docs(docs): """ This function formats the documents for insertion into the RAG prompt """ doc_template_string = dedent(""" <document source="{the_source}"> {content} </document>""") return "\n\n".join(doc_template_string.format(content=doc.page_content,the_source=doc.metadata["source"]) for doc in docs)#1. Setupmodel_to_use = "gpt-4o-mini"#...The retriever parts of the file have been omitted for clarity#4.0 Setting up the RAG chainrag_chain = ( {"context": retriever | format_docs, "question": RunnablePassthrough()} | our_prompt_template | model)#5.0 Calling the RAG chain in a loop to answer question in the terminalwhile True: user_question = input("Your question: ") rag_answer = rag_chain.invoke(user_question) print("\nBot's answer: {0}\n".format(rag_answer["answer"])) print("For more details see: {0}\n\n".format(rag_answer["sources"])) The complete RAG chatbot fileThe retriever code and the question-answering code are part of a file called rag_bot.py. Here’s the complete file.

```

This file has the chatbotimport osimport pickleimport promptsfrom textwrap import dedentfrom langchain_openai import ChatOpenAIfrom langchain_openai import OpenAIEmbeddingsfrom langchain_core.prompts import ChatPromptTemplatefrom langchain_community.vectorstores import FAISSfrom langchain_core.runnables import RunnablePassthrough#0. Function used to arrange the search results. Could be used in other code.def format_docs(docs): """ This function formats the documents for insertion into the RAG prompt """ doc_template_string = dedent(""" {content} """) return "\n\n".join(doc_template_string.format(content=doc.page_content,the_source=doc.metadata["source"]) for doc in docs)if name == "main": # Optional: Prompt the user for the API key #import getpass #os.environ["OPENAI_API_KEY"] = getpass.getpass() #1. Setup model_to_use = "gpt-4o-mini" docs_dir = "summarized_docs/" docs_pickle_file_name = "texts_and_embeddings.pkl" docs_pickle_file_path = os.path.join(docs_dir, docs_pickle_file_name) with open(docs_pickle_file_path, "rb") as p_file: text_embedding_metadata_triplets = pickle.load(p_file) #2.0 Setting the Langchain objects ##2.1 Setting up a Langchain "model" object model = ChatOpenAI(model=model_to_use).with_structured_output(None, method="json_mode") ##2.2 Setting up our prompt template our_prompt_template = ChatPromptTemplate.from_messages( [ ("system", prompts.rag_system_prompt), ("user", prompts.rag_user_prompt)] ) #2.3 Set up the embeddings model embeddings = OpenAIEmbeddings() #3.0 Setting up the retriever faiss_vectorstore = FAISS.from_embeddings(text_embedding_metadata_triplets["embeddings"], embeddings,text_embedding_metadata_triplets["meta"]) retriever = faiss_vectorstore.as_retriever() #4.0 Setting up the RAG chain rag_chain = ( {"context": retriever | format_docs, "question": RunnablePassthrough()} | our_prompt_template | model ) #5.0 Calling the RAG chain in a loop to answer question in the terminal while True: user_question = input("Your question: ") rag_answer = rag_chain.invoke(user_question) print("\nBot's answer: {0}\n".format(rag_answer["answer"])) print("For more details see: {0}\n\n".format(rag_answer["sources"]))

`` Putting it all togetherDirectory structureWe have three.pyfiles. Our three prompts go intoprompts.py.summarize_docs.pypreprocesses the documents.rag_bot.py` is the RAG chatbot that runs in terminal.

I have drawn the directory structure below. The semaphore folder is a clone of the new Semaphore git repo. I haven’t shown all of the files. We are working with the markdown files in the docs directory.

All of the paths that the chatbot displays are relative to semaphore/docs/. There is a second docs directory within semaphore/docs/. So semaphore/docs/docs/getting-started/guided-tour.md will be displayed as docs/getting-started/guided-tour.md.

📂Top_level_dir ┣ 📂docs_chatbot ┃ ┣ 📂summarized_docs ┃ ┃ ┗ 📜texts_and_ebeddings.pkl <== This file is generated by summarize_docs.py ┃ ┣ 📜__init__.py <== Optional. ┃ ┣ 📜prompts.py ┃ ┣ 📜rag_bot.py ┃ ┣ 📜requirements.txt ┃ ┗ 📜summarize_docs.py ┗ 📂semaphore <== This is a clone of the repo at https://github.com/semaphoreci/semaphore.git ┃ ┣ 📁.git ┃ ┣ 📁.github ┃ ┣ 📁.semaphore ┃ ┣ 📁docs <== This is the docs directory that we are working with ┃ ┣ 📜LICENSE ┃ ┗ 📜README.md Setting the OpenAI API KEYI have set the OpenAI API KEY as an evironment variable on my machine. I use the following code in the terminal. Note how I clear history.

export OPENAI_API_KEY="my_api_key_here_123"history -c Then I press ctrl+shift+L to clear the terminal window. Your keyboard shortcuts might be different.

Another option is for the python code to prompt the user for the API key. You can find some commented out code in summarize_docs.py and rag_bot.py.

```

Optional: Prompt the user for the API keyimport getpassos.environ["OPENAI_API_KEY"] = getpass.getpass()

`` Optional code that writes the summmaries and augmented context to disksummarize_docs.py` has some optional code that writes the summmaries and augmented context to disk. You can remove it if you don’t want it. These files are a copy of the text that we are storing in our retriever.

```

3.5 Optional: Write the sumarised file to disks_file_name = os.path.join(output_dir, our_path.replace("/","_"))with open(s_file_name, "w") as s_file: print(s_file_name) path_spec_text = "Path to file: {0} \n\n".format(our_path) s_file.write(the_summary)

``` Running the chatbotHere are the shell commands to preprocess the documents and run the RAG chatbot. I have put the code for this chatbot into a public git repo. So you can set up all of the files by just cloning two git repos.

git clone https://github.com/semaphoreci/semaphore.gitgit clone https://github.com/slavarazbash/docs_chatbot.git Then you will need to install the required packages. I recommend using a virtual environment.

CD docs_chatbotpython -m venv myenvsource myenv/bin/activatepip install -r requirements.txt These are the commands to preprocess the documents and start the chatbot.

python summarize_docs.pypython rag_bot.py Try it yourself. Please reach out if you have any questions or difficulties.

The post How to Build a RAG Chatbot to Chat with Documents Using Sparse Data appeared first on Semaphore.

View Details

As web users, we have all encountered “404” or “500” error pages at least once. Ever wondered what these digits mean? Well, those numbers are not casual and are known as HTTP status codes!

These are designed to communicate status information between servers and clients. While they appear as seemingly innocuous codes, attackers leverage them for vulnerability exploitation in backend applications.

In this guide, you will understand what information these HTTP status codes provide and how attackers exploit them to compromise your server.

Let’s dive in!

What Are HTTP Status Codes?HTTP response status codes are three-digit numbers that the server sets in the response to a client request. They represent a way to classify the outcome of an HTTP request and can be divided into five classes:

  • 1xx: Informational responses
  • 2xx: Successful responses
  • 3xx: Redirection messages
  • 4xx: Client error responses
  • 5xx: Server error responses

Specifically, HTTP error codes are all status codes that go from 400 to 599. These refer to situations where the request could not be successfully processed because of a client or server error.

Each status code provides different information to the caller, and it is up to the backend developer to set the right HTTP status error code for each particular error. For instance, a 400 Bad Request response indicates an invalid request from the client, while a 500 Internal Server Error signals a generic application-level error in the server.

Can Attackers Exploit HTTP Status Codes?TL;DR: Yes, HTTP error codes are generally exploited by attackers to perform sophisticated attacks on servers.

HTTP status codes provide information about the success or failure of a client’s request to a server. This is free information for attackers, who can monitor and study changes in HTTP response status codes to forge increasingly effective requests. This iterative process enables them to slowly bypass web application security measures and get closer to their malicious objectives.

For instance, attackers could build a bot that performs a brute force attack to automatically find all resources exposed by your server. Initial requests may result in 404 Not Found errors, but subsequent requests might produce 401 Unauthorized, 403 Forbidden, or even 200 OK responses, indicating public endpoints to attack.

Status codes, whether success or error, help attackers map out a backend application’s routes and identify potential security flaws. They reveal the server’s behavior in response to specific inputs, which is awesome feedback for attackers!

Why Returning Wrong HTTP Status Codes on Purpose Is Not A SolutionPrograming a backend application to return “wrong” HTTP status codes on purpose is a possible STO (Security Through Obscurity) technique. The idea is to make the public behavior of a server application more opaque as a way to enhance its security.

Setting unexpected HTTP status codes will confuse attackers and could disrupt most automated attacks, slowing down the progress of malicious users. At the same time, this tactic will confuse developers as well. Deviating from standardized HTTP error code conventions will also complicate troubleshooting and debugging. Plus, this STO strategy cannot be applied on public endpoints, where adherence to standards is crucial.

While returning uncommon HTTP response status codes may momentarily disorient attackers, it does not address the underlying vulnerabilities in the server. In other words, it is just a temporary deterrent that does not really mitigate attack vectors. Here is why you should not rely on such an exotic approach to web application security.

Most Interesting HTTP Status Code for AttackersTime to dig into the most interesting HTTP status codes that attackers track for malicious purposes. Let’s explore the common attacks associated with these response codes!

200The HTTP 200 OK success status response code indicates that the server has successfully processed the request. While this status code does not carry much information by itself, it signals to attackers that their attempt—whether automated or manual—was successful. In essence, it tells attackers that they have found a way to legitimately receive responses from the server. That opens the door to a world of possibilities, from data scraping to unauthorized access.

HTTP 200 OK responses are especially valuable to attackers when they target endpoints that should be protected by authorization. The Common Weakness Enumeration (CWE) describes this scenario as “Missing Authorization.”

For example, a 200 OK response from an unknown IP on paths like /admin/ or /private/ is highly interesting to a user with malicious purposes, as it suggests that these sensitive areas of the server are accessible without proper authorization.

400The 400 Bad Request status code indicates that the server cannot process the request due to a client error. This is generally caused by an invalid request message framing, deceptive request routing, or malformed request.

Attackers can use the 400 status code to test the robustness of a web application. By deliberately sending malformed or unexpected data in a process known as fuzzing, they can observe how the server handles such inputs. If the web application returns a 400 status code, it confirms that some input validation is in place. However, it might expose weak or inconsistent input validation behavior, which could be exploited further.

Additionally, 400 responses could include detailed error messages about the invalid input. These messages can inadvertently reveal sensitive information such as variable names used in the business logic, the data validation library in use, or even database schema details. Attackers can employ all that information to craft more specific attack vectors. This is why 400 error messages should always be as generic as possible.

A 400 status code can also be harnessed to understand whether an endpoint exists, but the request was improperly formed. This information is useful to discover API endpoints and resources on a server.

401The 401 Unauthorized status code indicates that the request has not been processed because it lacks valid authentication credentials for the target resource. Attackers typically send automated requests to various endpoints, looking for 401 responses. This helps them identify that an endpoint exists and requires authentication.

Once an attacker identifies the existence of an authenticated endpoint, they can initiate a brute force attack by systematically trying different username and password combinations. In some cases, backend applications return different 401 responses based on whether the username or password was incorrect (e.g., “Invalid username” vs “Invalid password”). Malicious users can exploit this to detect valid usernames. Instead, send fewer details back with the 401 (e.g. “Unknown username and/or incorrect password”).

401 responses are also valuable for studying the type of authentication (e.g., basic auth, bearer tokens, etc.) used by the server and the required parameters to satisfy it. With this information, it is possible to craft more sophisticated attack vectors aimed at exploiting weaknesses in the security authentication system of a web application.

403The 403 Forbidden error code indicates that the server understood the request but refused to authorize it. Attackers leverage this status code to identify which resources are restricted by the server. The next step involves attempting to gain unauthorized access to these resources by modifying cookies, forging authentication tokens, or tampering with URL parameters.

By analyzing how the server responds to different variations of the request, attackers may also gain insights into the application’s logic, potentially spotting weaknesses in the access control mechanism.

A 403 response could also indicate that directory listing is disabled, but the directory itself exists. This may inadvertently disclose the existence of files or directories that attackers can then target for further vulnerability exploitation.

404The 404 Not Found status code is likely one of the most common HTTP error codes for attackers, indicating that the server cannot find the requested resource.

By utilizing directory traversal or brute-force attacks based on 404 responses, attackers can trace out the server’s directory structure. A similar mechanism can be employed for performing endpoint and/or file enumeration. Thus, 404 error codes are useful for identifying which endpoints and resources exist and are accessible on a server.

In most cases, 404 responses consist of a generic HTML page that provides more information than just the basic “Not Found” message. These predefined web pages may also provide enough information for attackers to make informed guesses about the technology stack in use by the server. The solution here is to always override these default pages.

405The 405 Method Not Allowed status code indicates that the server does not support the HTTP method of the request.

Discovering the existence of an API endpoint means knowing its path and HTTP method (e.g., GET, POST, PUT, PATCH, or DELETE). 405 responses help an attacker find the right HTTP method after identifying a valid path the server listens to.

To hinder the attacker, in the 405 response message, do not specify the correct HTTP method the caller should use to reach the API endpoint.

429The 429 Too Many Requests error code indicates that the client has sent too many requests to the server in a given amount of time. That means that rate-limiting measures are in place. Attackers will then adjust their attack strategies accordingly, such as routing automated requests through a residential proxy network to get fresh exit IPs and avoid blocks.

Additionally, 429 responses may include RateLimit-* or X-RateLimit-* headers. These headers inform the client about the number of requests allowed in a given time frame, which is key information for attackers. Also, based on the names of those headers, it may even be possible to guess the library employed to implement rate limiting and consequently the technology stack in use.

The solution here is to respond with a 429 status code, without including rate-limiting headers or specific information about how your server limits incoming requests. In the case of requests routed by proxy servers, there is not much you can do, especially if they provide genuine IPs from residential devices. However, if the proxy IPs come from datacenters, their numbers are usually incremental, so they can be detected and banned in bulk.

500The 500 Internal Server Error status code is the most generic among HTTP error codes and indicates that the server failed to fulfill the request. The reason behind a 500 response may be countless, but responses might include error messages or even entire stack traces.

Those error messages typically provide useful information about the configuration or internal workings of the server. The ultimate goal of attackers is to use that information to identify vulnerabilities to exploit.

Also, keep in mind that backend frameworks and libraries typically return generic 500 Internal Server Error responses in case of server errors. The default messages in these responses can sometimes be enough to guess the server’s technology stack.

To avoid assisting the attacker, override the default message and always respond with generic errors. For example, a message like “Internal Server Error” is enough to explain any 500 errors.

Resources to Strengthen Your Security GameSecurity is a cat-and-mouse game, and if you want to stay ahead, continuous learning is essential. Equip yourself with these valuable resources:

  • OWASP Secure Coding Practices: A comprehensive list of community-backed best practices designed to protect against the most common security attacks. This guide is supported by The OWASP Foundation, a nonprofit organization focused on improving software security.
  • TryHackMe: A collection of hands-on labs that teach you how to attack web applications through interactive, real-world exercises. By stepping into an attacker’s shoes, you can gain a deeper understanding of their tactics and strategies.
  • SANS Cyber Security Training: It offers a wide range of courses and certifications covering various aspects of cybersecurity, from foundational concepts to advanced threat detection techniques. Their training is widely recognized and respected in the industry.

ConclusionIn this guide, you learned what HTTP codes are and how attackers can use them to carry out targeted attacks against your server or web application. An idea to avoid this problem is to start returning uncommon HTTP status codes on purpose. While that may seem like an effective approach against vulnerability exploitation, it is not a real solution. The real solution is to follow API security best practices.

The post How Attackers Use HTTP Status Codes for Malicious Purposes appeared first on Semaphore.

View Details

The cost of discovering flaws and security vulnerabilities in a production application can be staggering. When issues arise post-deployment, they can significantly damage your business’s reputation and undermine user trust. Given these challenges, many teams are adopting shift-left testing tools to enhance quality assurance early in the development process.

By moving testing activities “left” in the development timeline, you can identify and remediate issues before they escalate. This leads to high-quality, more stable, and secure applications.

In this article, you will understand how the shift-left testing approach works and then examine some of the best tools to implement it.

Let’s dive in!

What Is Shift-Left Testing?Shift-left testing is a practice that emphasizes moving testing activities early in the development process. By “shifting left” testing, developers and QA engineers can identify and fix issues and bugs sooner, when they are easier and cheaper to address:

This testing methodology typically happens during the design and coding phases of the software development lifecycle. It involves designing and implementing software that works as expected by following methodologies like Test-Driven Development (TDD).

Along with unit tests, integration tests, and E2E tests, shift-left testing also uses static code analysis (SCA) solutions. These tools scan code, configurations, dependencies, and settings in your repository to detect security vulnerabilities and other flaws. The ultimate goal is to ensure a more robust and reliable application before it reaches production, thereby improving the overall quality assurance (QA) process.

This approach to testing contrasts with shift-right testing, which focuses on testing later in the software lifecycle, closer to production. While shift-right testing relies on techniques like application monitoring to find bugs in live systems, shift-left testing occurs before the application is fully deployed and running.

Main Benefits of Shift-Left TestingShifting testing to the left in your software development cycle leads to several benefits, including:

  • Quick bug detection: Identify flaws as early as possible in the development cycle, allowing you to address and easily fix them before they become too big and complex. By preventing the accumulation of unresolved issues that can lead to long-term problems in your application, the shift-left testing approach reduces technical debt.
  • Improved software quality: Addressing vulnerabilities, code smells, and bugs before they reach production results in higher-quality code. A live application with fewer issues is also a more reliable product for your users.
  • Lower costs: Addressing issues earlier in the development process is generally much cheaper than identifying and resolving them later in production. Bugs can significantly slow down and frustrate developers, so facing them timely speeds up coding. The result is a quicker time-to-market, which ultimately reduces overall development costs.
  • Enhanced collaboration: Placing so much importance on testing from the beginning encourages developers and QA teams to collaborate closely from day one. This fosters a synergetic environment among development, security, and quality teams.
  • Strengthened security: Continuous analysis of code, configurations, and dependencies helps mitigate security risks before it is too late. For more guidance, read our detailed guide on security best practices.

Recommended Shift-Left Testing ApproachShift-left testing is a process that encompasses several facets of software testing, such as:

  • Manual tests: Scripts written by developers to validate the logic and functionality of the code. These include unit tests, performance tests, integration tests, automated tests, and more.
  • Dependency inspection: Examine third-party libraries and packages your application relies on to identify known vulnerabilities and make sure they are up to date.
  • Configuration inspection: Verify adherence to security best practices by evaluating the settings of the libraries and technologies used by your application.
  • Code quality analysis: Analyze source code for quality issues, such as using variables instead of constants, and potential security vulnerabilities, such as SQL injection, without executing it.

The recommended shift-left testing approach is to adopt a set of dedicated tools to cover each of the aspects mentioned above. This ensures thorough coverage and enables you to efficiently and effectively achieve all the benefits introduced earlier.

Keep in mind that—apart from manual tests written by your developers—this approach to testing is mainly carried out by static code analysis (SCA) tools. In particular, shift-left testing tools automatically inspect your codebase, examining all its files for bugs, flaws, and security vulnerabilities.

Since your code or configurations may contain credentials or sensitive information related to your business, those automated tools should be open-source. This way, the community can verify how they operate and make sure they are not stealing data or engaging in other misconduct.

Top 5 Open-Source Shift-Left Testing ToolsExplore some of the best and most useful shift-left testing tools available on the market. For each tool, we will analyze these aspects:

  • Testing scenarios covered: Manual testing, dependency inspection, static code analysis, etc.
  • Supported programming languages: The programming languages that the tool supports.
  • Types of applications supported: Web applications, desktop applications, mobile apps, etc.
  • GitHub stars: The number of stars the project has received on GitHub.
  • Downloads: The number of times the tool has been downloaded from dependency registries like npm.
  • Pros: The main advantages of the tool.
  • Cons: The main disadvantages of the tool.
  • Pricing: The cost of the tool for business or enterprise needs, if applicable.

If you are eager to see how these tools compare, take a look at the summary table below:

| Tool | Goal | Supported Programming Languages | Supported Applications | GitHub Stars | Downloads | Open-Source Version | Premium Version | | --- | --- | --- | --- | --- | --- | --- | --- | | Automated Test Frameworks | Write and run test scripts | Depends on the framework (e.g., JavaScript, Python, Java, etc.) | Web, desktop, mobile applications | Varies | Varies | ✅ | Varies | | OSV-Scanner | Vulnerability scanning | C, C++, Dart, Elixir, Go, Java, JavaScript, PHP, Python, R, Ruby, Rust | Web applications | 6k | Undisclosed | ✅ | ❌ | | TruffleHog | Secret scanning and detection | Any | Any | 15.8k | Undisclosed | ✅ | ✅ | | Snyk | Vulnerability scanning across code, dependencies, containers, and IaC | Bazel, C/C++, Dart, Elixir, Go, Java, Kotlin, JavaScript, .NET, PHP, Python, Ruby, Swift, TypeScript, VB.NET | Web, desktop, mobile applications | 4.9k | 550k+ weekly | ✅ | ✅ | | Checkov | Misconfiguration scanning for IaC | Any | Web applications | 7k | 1+ million weekly | ✅ | ❌ |

Time to dig into these tools (in random order)!

Automated Test Frameworks

More than a specific shift-left testing tool, this represents a category of tools. Specifically, it encompasses all frameworks you can use to write manual test scripts in your codebase. Examples of such solutions include Selenium, pytest, Playwright, Jest, Mocha, and JUnit.

In most cases, these technologies come with everything you need to implement one or more specific types of tests in your application. They offer an API for writing tests, assertion libraries, test runners, test reporting capabilities, and additional features.

For additional details, check out our software engineering testing guides.

🧪 Testing scenarios covered: Manual testing via unit tests, integration tests, smoke tests, component tests, E2E tests, and more

💻 Supported programming languages: Depends on the chosen tool, but there is at least one testing framework available for all major programming languages.

🌐 Types of applications supported: Web applications, desktop applications, mobile apps, and more

GitHub stars: Some of these tools, like Playwright, have over 65k stars

📥 Downloads: Some tools, such as Jest, have over 24 million weekly downloads

👍 Pros:

  • All-in-one testing solutions that provide complete testing capabilities
  • Integration with CI/CD pipelines
  • Strong community support

👎 Cons:

  • Some tools have a steep learning curve

💰 Pricing: The great majority of these tools are free, but some offer advanced features for a fee. For example, Cypress comes with a solution with premium features called Cypress Cloud

OSV-ScannerOSV-Scanner is a vulnerability scanner developed by Google in Go that uses data provided by OSV.dev, a distributed open-source vulnerability database.

OSV-Scanner analyzes your project’s list of dependencies, connects to the OSV.dev database, and reports any vulnerabilities that affect them. Compared to scanners based on closed-source databases, the open nature of OSV.dev ensures that each advisory is sourced from a reliable and public platform. Also, this guarantees that anyone can contribute to these advisories.

🧪 Testing scenarios covered: Dependency inspection for detecting security vulnerabilities

💻 Supported programming languages: C, C++, Dart, Elixir, Go, Java, JavaScript, PHP, Python, R, Ruby, and Rust

🌐 Types of applications supported: Web applications

GitHub stars: 6k stars

📥 Downloads: Undisclosed

👍 Pros:

  • Work with multiple programming languages and package indexes
  • Developed and maintained by Google
  • Based on an open-source and distributed database of vulnerabilities

👎 Cons:

  • Over 100 issues currently reported on GitHub

💰 Pricing: Free

TruffleHogTruffleHog is a secret scanning tool that searches deep into code repositories, version histories, S3 directories, Docker images, and more to detect hidden secrets like passwords, API keys, and sensitive credentials.

TruffleHog also features preventive measures, using hooks to stop secrets from being committed, and offers remediation tools with alerts and guides. It minimizes false positives by validating detected credentials.

🧪 Testing scenarios covered: Secret detection

💻 Supported programming languages: Any

🌐 Types of applications supported: Any

GitHub stars: 15.8k stars

📥 Downloads: Undisclosed

👍 Pros:

  • GitHub, S3, directory, GCS, and Docker scanning with 800+ secret detectors available
  • Credential verification to avoid false positive
  • Multi-branch analysis capabilities

👎 Cons:

  • Most powerful features are available only in the premium Enterprise version

💰 Pricing:

  • Open-Source: Free
  • Enterprise: Premium features available at an undisclosed price (contact the support team for pricing)

SnykSnyk is a developer-first, cloud-native security platform that helps identify and fix vulnerabilities across various aspects of software development. The products it offers are :

  • Snyk Open Source: Detect and fix open-source vulnerabilities.
  • Snyk Code: Identify and resolve security issues in application code.
  • Snyk Container: Secure container images and Kubernetes applications.
  • Snyk Infrastructure as Code: Address insecure configurations in Terraform and Kubernetes code.

You can integrate the Snyk CLI into your workflow, IDE, or CI/CD pipeline for seamless security testing.

🧪 Testing scenarios covered: Vulnerability detection in dependencies, code, containers, and configurations

💻 Supported programming languages: Bazel, C.C++, Dart and Flutter, Elixir, Go, Java, Kotlin, Javascript, .NET, PHP, Python, Ruby, Swift, Typescript, and VB.NET

🌐 Types of applications supported: Web applications, desktop applications, and mobile apps

GitHub stars: 4.9k stars

📥 Downloads: 550k+ weekly downloads

👍 Pros:

  • Support for multiple shift-left security tools
  • Simple CI/CD integration
  • Dedicated plugins for integration directly in popular IDEs like Visual Studio Code, Intellij IDEs, and Eclipse

👎 Cons:

  • Requires a Snyk account to work
  • Not a fully open-source vulnerability database

💰 Pricing:

  • Free: For individual developers and small teams (free forever)
  • Team: For development teams looking to build security (contact sales for pricing)
  • Enterprise: For standardizing developer-first security across an organization (contact sales for pricing)

CheckovCheckov is an open-source security tool that scans cloud infrastructure configurations for misconfigurations. It helps identify security and compliance issues in Infrastructure as Code (IaC) platforms, preventing potential vulnerabilities from being introduced into cloud environments.

The IaC providers supported by Checkov features Terraform, Terraform plan, CloudFormation, Kubernetes, ARM Templates, Serverless, Helm, and AWS CDK.

In addition to misconfiguration scanning, Checkov can also detect vulnerabilities in open-source packages and container images.

🧪 Testing scenarios covered: Code misconfiguration detection, dependency vulneraribility detection

💻 Supported programming languages: Any

🌐 Types of applications supported: Web applications

GitHub stars: 7k stars

📥 Downloads: 1+ million weekly downloads

👍 Pros:

  • Comes with over 1,000 built-in policies
  • Easy integration into existing developer workflows
  • Trusted by Prisma Cloud Application Security

👎 Cons:

  • Requires Python to work

💰 Pricing: Free

Honorable MentionsOther shift-left testing tools worth mentioning are:

  • Trivy: A security scanner that checks for vulnerabilities in container images, file systems, and Git repositories. It can also execute IaC scanning to detect misconfigurations. Discover more in our tutorial on continuous container vulnerability testing with Trivy.
  • SonarQube: A tool for continuous code quality and security inspection. It analyzes source code to detect code smells, bugs, and security vulnerabilities across multiple programming languages. Find out more in our guide on SonarQube.

ConclusionIn this guide, you learned about the importance of integrating testing as early as possible in the development lifecycle. As demonstrated, this methodology offers significant benefits, such as reduced costs and faster release times. However, achieving these advantages requires the right tools. Here, you discovered the best open-source shift-left testing tools to facilitate the automation of testing in applications still in development.

The post Best Shift-Left Testing Tools To Improve Your QA appeared first on Semaphore.

View Details

Open-source software is the building materials of the vast majority of digital technology as we know it. Anyone can access and contribute to it, which is great for flexibility and cost, but also means there’s a risk of hidden flaws. In this episode, Stacklok’s CEO Craig McLuckie shares his tools to check the quality and security of open-source software production. Working together as a community, we can make open-source software safer and more reliable for everyone.

Edited transcription

As CEO and co-founder of Stacklok, a software company specializing in supply chain security for open-source software, Craig McLuckie has closely witnessed a shift in the nature of hackers’ methodologies. Accidental vulnerabilities, in which “someone just didn’t anticipate a buffer overflow here, bad stuff happens there,” have given up their place to “a world where hostile actors are actually trying to manipulate the supply chain.” Consequently, security is moving from finding and fixing vulnerabilities from external threats to preventing malicious contributors from creating, nurturing, and preserving vulnerabilities.

With the recent antecedent of the XZ Utils backdoor as the incident of the highest profile, Craig points out that despite the “high importance, high repackaging, high reuse” of open-source “critical supporting technologies,” these, in turn, have “relatively low maintainer engagement.” As the sophistication, dedication, and scale of this attack raise strong suspicions of state-backed actors behind them, these vulnerabilities are not easy to detect, nor to determine their reach: “With these things, you never know what the blast radius is really,” Craig adds.

The ubiquity of open-source softwareSecurity threats of the magnitude of the XZ vulnerability put under the spotlight one of the main positive qualities of open-source software: Collaboration. Fair enough, it may lead to fear and distrust in the open-source community, as free comes with a cost.

However, according to Craig, there’s no such thing as “non-open source” in today’s industry, in which virtually all software relies on open-source components at some level. Whether it’s the programming languages themselves (like Java or Python) or the underlying libraries, frameworks, and operative systems, “open source is basically the underpinning of everything that we build.

Moreover, Craig argues that when companies choose to work exclusively with proprietary vendors, what they’re really doing is engaging in a “risk adjustment process” or “arbitraging risk.” In other words, these companies aren’t actually avoiding open source; they’re merely shifting the responsibility for managing open source risks to their vendors. However, according to him, this perspective is flawed in as much as the lack of transparency in these vendor relationships doesn’t reduce risk exposure.

The SolarWinds incident, in which a provider of system management tools for network monitoring was compromised by attackers’ malicious code inserted into updates of its platform, is an example of how even when companies choose to work exclusively with proprietary vendors, they are still exposed to the risks associated with open-source software. Even when working with a closed-system vendor, that vendor is still consuming open-source technology—but the customer doesn’t necessarily know what components are being used, how they’re being used, or where they originate from. Hence, these companies are essentially choosing to close their eyes and trust that vendors are behaving responsibly, rather than actively managing these risks themselves.

Stacklok: Addressing supply chain securityCollaboration just goes away when there’s fear in the ecosystem,” Craig affirms. Still, he is confident that solving the complex issues of software supply chain security requires collaboration from many parties, not just a single vendor or company. Aligned with this philosophy, Craig envisions Stacklock as a “seed crystal” fostering an active community rather than remaining a solitary solution provider.

Stacklok was born out of Craig’s firsthand experience with enforcing supply chain security standards and controls at large tech companies like Google and VMware. Recognizing this as a widespread issue in the open-source community, Craig sought a solution that would provide greater transparency and traceability. Exploring the potential of employing a generic system for labeling software resources, he eventually came across Luke Hein, the creator of Sigstore. Sigstore’s innovative approach to software signing, which “connects a piece of software with information about its inception,” aligned perfectly with Craig’s vision of empowering users to understand the origins and trustworthiness of the software they rely on.

Setting up policies with MinderThe idea behind Stacklok was to give users ”the ability to apply policy” at any stage of the software development life cycle, ensuring that only software meeting predefined standards would integrate into their projects. To this end, Stacklok’s platform Minder enables multiple controls across the entire software development and deployment process. For example, setting policies around Git repositories, to each merge request, container registries, and Kubernetes deployments.

If you’re building software with dependencies, Craig recommends enforcing a policy “that blocks a merge request if there’s a vulnerability” or unfavorable attribute in it, like “bad licensing material, bad reputational scores, or with known CVEs.” Securing dependencies can save developers countless headaches. ”The last thing you want to do is discover a vulnerability and call a scan in production,” he says. As such, he calls to “intercept these things as quickly and early as possible, ideally in the IDEs.”

Minder can also wrap policies around Github actions and repositories, “and set up controls so that your organization can use or not use actions based on where they’re sourced from.” What’s more, you can write your own rules using YAML or Rego policy language.

Judging dependencies with TrustyWhile Minder is useful for setting up granular policies, Stacklock’s second product, Trusty, employs sophisticated data science techniques to assist organizations in evaluating the sustainability of software. By providing “high-quality signals intelligence,” Craig explains, Trusty empowers organizations to assess their hypotheses and make informed decisions about the software they adopt, proactively identifying potential sustainability issues before they escalate.

Signals are pieces of information that provide insight into the quality, security, or reliability of a software package or component. According to Craig, it is necessary to not take signals as a given, but to enforce a signaling process or source we can trust: “Unless you actually have a signature, unless you have some firm point of attestation that an identity that’s known to be associated with this package is the identity that published the package and published the signature of the package, you just don’t know.

Craig asserts that a technology like SIGSTOR is essential for determining the responsible sourcing of the products or services we consume. However, he acknowledges that implementing such technology can be more challenging in open-source projects. In this way, even in the absence of SIGSTOR, Craig believes that Trusty valuable intelligence signals can assist in assessing responsible sourcing.

For signaling packages, Trusty uses historical provenance. Trusty examines the repo and checks associated release tags to find a chain of trust back to the source code: “If the tags and the sort of known versions associated with this package lineup is probably the package you’re looking for,” Craig explains. “If they don’t, it probably isn’t—we would then give it a very low score because we don’t have proof of its historic provenance.”

Trusty also performs principal component analysis, which analyzes a database of bad packages and compares their composition with good packages. According to Craig, this analysis allows users to “build a statistical model so that when we’re looking at something that we’ve never seen before, we can sort of attest whether it’s good or bad.” Trained AI can find patterns that can be correlated with packages falling out of reputation, such as a lack of maintainers. In these cases, the platform will recommend better alternatives.

The bottom lineStacklok’s roadmap includes new features for private repositories and introduces commercial offerings. Also, while currently focused on GitHub integration, Stacklok is working on expanding to other providers, such as OCI registry provider and Kubernetes provider to continue introducing control points across various stages of the software development lifecycle.

Visit Stacklok.com and github.com/stacklok to learn more about the project and test its product. Join Stacklok’s Discord community to chat directly with its engineers and find out the latest about its products’ development.

Follow Craig on X at @cmcluck. To learn more about his views on supply chain security, check his talk at the Open Source Software Summit Seattle 2024.

The post Craig McLuckie on Enforcing Compliance on Open Source Development appeared first on Semaphore.

View Details

Keeping users engaged is crucial to any application’s success. Automated email summaries provide an effective way to achieve this by delivering curated content that keeps users connected to your platform. In this tutorial, we’ll guide you through building an AI-enabled automated email summary system with continuous integration and deployment (CI/CD) using Semaphore.

PrerequisitesBefore starting this tutorial, you should have:

  • Node.js version 20 or higher
  • GitHub account for version control
  • Semaphore account for CI/CD setup
  • Anthropic account for API access to Claude
  • Ethereal Email account for creating a test email account for mocking

All of the code for this article is available at ajcwebdev/semaphore-ai-email. Begin by cloning this repo, creating a .env file, and installing the project’s dependencies:

git clone https://github.com/ajcwebdev/semaphore-ai-emailcd semaphore-ai-emailcp .env.example .envnpm i Step 1: Setting Up the Email Service and TemplatesWe’ll begin by creating the foundation of our email summary system: the email service and templates. This step involves setting up Nodemailer for email sending and designing responsive HTML templates for our summaries.

First, open the file named send-email.js. This script will handle the email sending process:

// send-email.jsimport nodemailer from 'nodemailer'import { fileURLToPath } from 'url'import { dirname, join } from 'path'import fs from 'fs/promises'const __filename = fileURLToPath(import.meta.url)const __dirname = dirname(__filename)async function sendEmail() { let transporter = nodemailer.createTransport({ host: process.env.EMAIL_HOST, port: 587, auth: { user: process.env.EMAIL_USER, pass: process.env.EMAIL_PASS } }) const htmlTemplate = await fs.readFile(join(__dirname, 'email-template.html'), 'utf-8') let mailOptions = { from: `"Test Email" <${process.env.EMAIL_USER}>`, to: process.env.EMAIL_USER, subject: 'Weekly Update', html: htmlTemplate } let info = await transporter.sendMail(mailOptions) console.log('Message sent: %s', info.messageId) console.log('Preview URL: %s', nodemailer.getTestMessageUrl(info))}async function main() { try { await sendEmail() console.log('Email sent successfully. Waiting 1 week before sending the next one...') } catch (error) { console.error('Failed to send email:', error) } await new Promise(resolve => setTimeout(resolve, 7 * 24 * 60 * 60 * 1000))}main().catch(console.error) This script sets up a Nodemailer transporter using Ethereal Email, a catch-all email testing service. Ethereal is ideal for development as it captures emails without actually sending them, providing a preview URL for each message.

Let’s break down the key components:

  • Imports and setup: We import necessary modules and set up file path utilities.
  • sendEmail function: Creates a Nodemailer transporter, reads our HTML template and CSS file, and sends the email.
  • File reading: We use fs.promises to read the template and CSS files asynchronously.
  • Email configuration: The email is configured with a from address, to address, subject, HTML content, and CSS attachment.
  • Logging: After sending, we log the message ID and preview URL provided by Ethereal.
  • main function: Runs in a loop, sending an email once every week. This simulates a scheduled task, which we’ll implement properly with Semaphore later.

This script references two articles selected and saved in files called ARTICLE_1.js and ARTICLE_2.js. You can use the examples provided in my repo or replace them with your own. The files are structured like so (see example repo for entire file contents):

`` // ARTICLE_1.jsexport const ARTICLE_1 = { title: "Why you should write your own LLM benchmarks - with Nicholas Carlini, Google DeepMind", content:Today's guest, Nicholas Carlini, a research scientist at DeepMind, argues that we should be focusing more on what AI can do for us individually, rather than trying to have an answer for everyone....,}// ARTICLE\_2.jsexport const ARTICLE\_2 = { title: "Why you should write your own LLM benchmarks - with Nicholas Carlini, Google DeepMind", content:Betteridge's law says no: with seemingly infinite flavors of RAG, and >2million token context + prompt caching from Anthropic/Deepmind/Deepseek, it's reasonable to believe that \"in context learning is all you need\".

...,}** ``` **Next, open the HTML email template namedemail-template.html. The AI summaries we’ll generate in the next section are contained within themain` tags.

`` **Weekly AI Newsletter Weekly AI Newsletter Your weekly digest of AI news and insights

Top Articles Why you should write your own LLM benchmarks — with Nicholas Carlini, Google DeepMind The article features an interview with Nicholas Carlini, a research scientist at DeepMind, discussing his views on AI and his work in AI security. Key points include:

  • Carlini wrote a viral blog post "How I Use AI" detailing practical ways he uses AI tools in his daily work as a security researcher. He emphasizes focusing on what AI can do for individuals rather than trying to have a universal answer.
  • He created a domain-specific language for writing personalized LLM benchmarks, arguing that more people should make benchmarks tailored to their specific needs rather than relying solely on standardized tests.
  • Carlini discusses his research in AI security, including work on data poisoning, model stealing, and extracting training data from language models. He emphasizes the importance of studying real-world vulnerabilities in AI systems.
  • He explains his preference for focusing on attacking rather than defending AI systems, stating that he finds it more motivating and believes he can contribute more effectively by identifying vulnerabilities.

Is finetuning GPT4o worth it? — with Alistair Pullen, Cosine (Genie) This article discusses an interview with Alistair Pullen, CEO and co-founder of Cosine, about their new AI coding agent called Genie. Key points include:

  • Genie achieved state-of-the-art results on the SWE-Bench coding benchmark, outperforming other solutions by a significant margin.
  • Cosine worked closely with OpenAI to fine-tune GPT-4 on billions of tokens of synthetic code data, focusing on reproducing the process of how human software engineers work rather than just training on finished code.
  • Genie uses a four-stage workflow: finding files, planning actions, writing code, and running tests. It can interact with GitHub and CI systems to execute and test code changes.
  • The team faced challenges in areas like code retrieval, context window limitations, and balancing different programming languages in the training data.
  • Cosine is looking to expand Genie's capabilities, increase their dataset size, and potentially customize models for specific customers' codebases.

© 2024 Weekly AI Newsletter. All rights reserved.

Unsubscribe | View in browser | Privacy Policy` ``` This HTML template provides the structure for our email. It includes:**

  • *Responsive meta tag* for proper rendering on mobile devices
  • *Header* with the newsletter title
  • *Main content area* with placeholders for article summaries
  • *Footer* with copyright information and standard email links

The CSS defines the styles for our email template. Key features include:

  • *Media query* for responsiveness on smaller screens
  • *Consistent typography* and color scheme
  • *Container* with a maximum width for better readability
  • *Distinct styling* for header, content sections, and footer
  • *Subtle use* of background colors and padding to separate different areas of the email

In the next step, we’ll focus on content aggregation and processing, where we’ll use AI to generate summaries for our newsletter.

Step 2: Content Aggregation and ProcessingIn this step, we’ll use Anthropic’s Claude API to automatically summarize articles for our email digest. This process involves interacting with an advanced language model to generate concise, informative summaries of longer articles.

Open the file named claude.js:

**`// claude.jsimport Anthropic from '@anthropic-ai/sdk'import { ARTICLE_1 } from './ARTICLE_1.js'import { ARTICLE_2 } from './ARTICLE_2.js'const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY})async function summarizeArticle(article) { const prompt = `Please provide a one to two paragraph summary of the following article:${article}Summary:` const response = await anthropic.messages.create({ model: "claude-3-5-sonnet-20240620", max_tokens: 300, messages: [ { role: "user", content: prompt } ] }) return response.content[0].text}async function processArticles(articles) { const summaries = [] for (const article of articles) { const { title, content } = article const summary = await summarizeArticle(content) summaries.push({ title, summary }) console.log(`Summarized: ${title}`) } return summaries}async function main() { const articles = [ { title: ARTICLE_1.title, content: ARTICLE_1.content }, { title: ARTICLE_2.title, content: ARTICLE_2.content } ] try { const results = await processArticles(articles) for (const result of results) { console.log(`\nTitle: ${result.title}`) console.log(`Summary: ${result.summary}`) console.log("-".repeat(50)) } } catch (error) { console.error("An error occurred:", error) }}main()`** Let’s break down this script and examine its key components:

  1. *Importing Dependencies and Articles:* We import the Anthropic SDK and two article contents from separate files. In a production environment, you might fetch these articles from a database or API.
  2. *Initializing the Anthropic Client:* We create an instance of the Anthropic client, using an API key stored in an environment variable for security.
  3. *Article Summarization Function:* The summarizeArticle function takes an article’s content and sends it to Claude with a specific prompt. We use the “claude-3-5-sonnet-20240620” model and limit the response to 300 tokens for concise summaries. The function returns the generated summary.
  4. *Article Processing Function:* The processArticles function iterates through an array of articles, calling summarizeArticle for each one. It collects the title and generated summary for each article and returns an array of these summaries.
  5. *Main Execution Function:* The main function defines an array of articles (in a real-world scenario, this data might come from an external source). It then calls processArticles and logs the results. Error handling is implemented to catch and log any issues that occur during processing.
  6. *Execution:* Finally, we call the main function to start the summarization process.

Open the .env file and include your Anthropic API key:

**`ANTHROPIC_API_KEY=""`** Run the script with the following command:

**`npm run claude`** This script demonstrates using AI for content processing. By using Claude’s natural language understanding capabilities, we can automatically generate concise summaries of lengthy articles. The next step will involve setting up automated deployment with Semaphore.

Step 3: Automating the Build and Deploy Process with SemaphoreIn this step, we’ll set up a continuous integration and deployment (CI/CD) pipeline using Semaphore. This will automate our build process and schedule our weekly email digest.

The semaphore.yml file in the .semaphore directory contains the specific steps for sending the email. This allows separating the weekly email task from the regular CI/CD pipeline.

**`# .semaphore/semaphore.ymlversion: v1.0name: Send Weekly Emailagent: machine: type: e1-standard-2 os_image: ubuntu2004blocks: - name: Send Email task: secrets: - name: email-credentials jobs: - name: Send Email commands: - checkout - sem-version node 20 - npm install nodemailer - node send-email.js`** 1. *Set up Semaphore project:* To set up your Semaphore project, first create a Semaphore account, open your organization settings, and navigate to the Secrets section:

  • 2. Create secrets: Click “New Secret” and name the secret email-credentials. Set your test email to EMAIL_USER, your password to EMAIL_PASS, and your email host provider to EMAIL_HOST.

  • 3. Save and connect: Click “Save Secret.” Next, create a new project and connect the project to your GitHub repository:

  • 4. Run workflow: This will start running your workflow:

  • 5. View logs: Click “Send Email” to view the workflow logs:

  • 6. Check results: Your logs should include a message saying, “Message sent” along with a preview URL. Open the preview URL to view the example email:

This separate pipeline file focuses solely on the tasks required to send your weekly email. By setting up this Semaphore configuration, you’ve automated your build process and scheduled your weekly email digest. Remember to regularly review your Semaphore logs to ensure everything is running smoothly and to catch any potential issues early.

Step 4: Analyzing and Optimizing Summary EmailsTo improve our email summaries over time, we should implement analytics to track open rates and engagement. We can set up A/B testing for subject lines and content layouts. Here’s how:

  1. *Create* multiple versions of your email template.
  2. *Modify* your Semaphore pipeline to randomly select a template version for each send.
  3. *Use* an analytics service to track which versions perform better. Popular options include Optimizely and VWO.
  4. *Periodically review* the data and update your templates based on the results.

Step 5: Scaling, Cost Optimization, and DeliverabilityAs your user base grows, consider these strategies for scaling:

  • *Use* a message queue (like RabbitMQ or Redis) to handle increased load.
  • *Implement* caching to reduce API calls and improve performance.
  • *Optimize* cloud resource usage.

A caching mechanism can significantly reduce the number of API calls to Claude, lowering costs and improving response times.

To ensure email deliverability and prevent your domain from being blacklisted for spam, implement the following best practices:

  1. *Monitor delivery metrics:* Keep a close eye on your email analytics, particularly failed deliveries and spam rejections. Use tools like SendGrid or Mailgun that provide detailed delivery reports.
  2. *Maintain list hygiene:* Regularly clean your mailing list by removing inactive subscribers and hard bounces. This improves your sender reputation and reduces the risk of being flagged as spam.
  3. *Implement authentication:* Set up SPF, DKIM, and DMARC records for your domain to verify your identity as a legitimate sender.
  4. *Optimize content:* Avoid spam trigger words in your subject lines and content. Use a balanced text-to-image ratio and include an easy unsubscribe option.
  5. *Warm up your IP:* If you’re using a new IP address, gradually increase your sending volume to establish a positive reputation with ISPs.
  6. *Use feedback loops:* Subscribe to feedback loops offered by major ISPs to receive notifications when recipients mark your emails as spam.
  7. *Test before sending:* Use tools like Mail-Tester to check your email’s spam score before sending it to your entire list.

By implementing these practices and closely monitoring your email performance, you can maintain a strong sender reputation, ensure high deliverability rates, and prevent your domain from being blacklisted.

ConclusionWe’ve built an AI-enabled automated email summary system with CI/CD capabilities. This system aggregates content, uses AI to generate summaries, and sends personalized email digests to users. By leveraging Semaphore’s CI/CD pipeline, we ensure consistent, automated deployments.

To maintain and evolve this system:

  • *Regularly update* dependencies and security patches.
  • *Monitor* performance metrics and user engagement.
  • *Continuously refine* your AI prompts for better summaries.
  • *Consider adding* more content sources and personalization features.

Remember, while we used Ethereal Email for testing, you’ll want to switch to a production-ready email service provider when you’re ready to send to real users.

Start building your own automated email summary system today by setting up your Semaphore pipeline and integrating AI-powered content summarization.

Additional ResourcesTo further your understanding of the technologies and concepts used in this tutorial, consider exploring these resources:

  • Semaphore Documentation: For in-depth information on CI/CD with Semaphore.
  • Anthropic’s Claude API Documentation: To learn more about using AI for content generation.
  • Node.js Documentation: For comprehensive information on Node.js development.
  • Nodemailer Documentation: To dive deeper into email sending with Node.js.

By following this tutorial and exploring these additional resources, you’ll be well-equipped to create and maintain a sophisticated, AI-powered email summary system that keeps your users engaged and informed.

The post Building an AI-Enabled Automated Email Summary System with CI/CD appeared first on Semaphore.

View Details

In terms of creativity, diversity, and evolution pace, generative AI seems boundless. However, its enterprise implementation remains significantly limited. Businesses are uncertain about making the right choices around AI and struggle to familiarize themselves with the potential of this technology amid cutting through the fragmented offerings from major tech companies. In this episode, Arun Gupta, VP and GM for Open Ecosystem at Intel, tells us about the company’s contributions to the open-source community and shares his experience at the Open Platform for Enterprise AI (OPEA) developing open-platform AI solutions for businesses.

Edited transcription

Arun Gupta’s extensive experience driving cultural change toward open source software includes working at Apple (where he built their first open source program office), Amazon, Red Hat, and Sun Microsystems — and later Oracle, after Sun was acquired. Currently, he is the vice president and general manager of Intel’s Open Ecosystem, and consequently, one of the company’s leading voices on everything open source.

Across industries and computer technologies, companies rely on open-source software and expect it to work flawlessly out of the box. “That’s where Intel engineers go out in these open source communities,” says Arun, explaining that Intel’s involvement in open source is about satisfying its diverse client base across verticals and making sure the features are working as intended.

Contributing to the open-source community is as much part of Arun’s job as part of Intel’s DNA. “We have been the largest corporate contributor to Linux since 2007. We are one of the top contributors to Kubernetes. We participate in 300 plus open source projects: Kubernetes, OpenJDK, PyTorch, TensorFlow, GCC, Clang,” he affirms. Arun also points out that Intel’s participation in “700 plus standard bodies and foundations,” benefits the sustainability of the open-source community as much as the company itself.

Foundational leadership in community projectsAside from code itself, Intel contributes to the sustainability of open source projects largely through foundational leadership. The company has recently launched the Unified Acceleration Foundation, which aims to create an open standard for accelerated computing. Intel is also “a premier member” of the LF AI & Data foundation, and holds a seat on the governing board. “The effort is to build and support an open AI and data community and data-driven open source innovation in the AI and data domains,” Arun explains.

From its leadership position, Arun argues, Intel can “strategically shape the direction and initiatives of the foundation […] and technically, strategically, administratively, financially steer the direction to represent our customers’ and developers’ interest.

Advocating for enterprise AI: The OPEA projectDespite the popularity and growing demand for AI solutions, businesses are still in the early stages of learning how to leverage them. Diving into this new field brings up a lot of inquiries about use cases, security, and the fittest implementation approach. Big tech companies, for their part, are aware of the lack of consensus and unfamiliarity around enterprise AI implementation and want to capitalize on it. “Every hyper scaler is crafting their own bespoke solution and saying, ‘We know it the best’,” says Arun.

To remedy this gap, Arun is currently deeply involved in the Open Platform for Enterprise AI (OPEA) project, an open platform initiative under the LF AI & Data foundation, aimed at simplifying and freeing the development of enterprise AI applications.

As a development framework, OPEA can be used to build gen AI solutions, such as chatbots, providing microservices components like embedding or data storage and the possibility of creating LLMs from your data with improved accuracy using Retrieval Augmented Generation (RAG). As a complementary feature, OPEA offers assessments to evaluate the readiness and trustworthiness of applications and determine at what stage of development they are.

As in the case of AI itself, Arun affirms the project is growing at a marvelous pace: “We launched the project three and a half months ago now. At that time, we had 14 launch partners. We are now up to more than 40 launch partners.” Among these partners are prominent actors such as Canonical, Infosys, Neo4j, Prediction Guard, and Docker, whose interest ranges across different types of AI applications fitting their business use case.

To balance this growth speed and array of contributors, the project has a technical steering committee with members across “a wide range of the community,” says Arun, including a couple from Intel, since “that’s where the largest code contribution is coming in from.” Still, to keep transparency, he keeps his project suggestions and issues in the Github repository, rather than hidden away in “ internal project meetings.” “I just fight the issue on GitHub,” says Arun, “because that’s part of the cultural change that we need to bring, that if the leaders of the project are going to do some sidebar discussions, how would people outside the company understand?

Regarding deployment, developers looking to get started quickly can package OPEA’s components as Docker containers and publish them in Docker Hub. This containerization strategy forms the backbone of OPEA’s deployment flexibility. “As we are building these microservices, using containers as a base layer for the microservices allows us to run it on any cloud-native platform,” Arun explains. Moreover, OPEA’s deployment options extend far beyond. “You could get this up and running on Kubernetes, for example, your EKS or GKE or AKS, doesn’t matter, pick your favorite flavor of Kubernetes, and we can get it up and running over there,” Arun explains.

The bottom lineOPEA is preparing a hackathon and inciting students and professionals worldwide to participate. If you want to be even more involved, OPEA also has working groups dedicated to different areas such as security, end users, and community. To learn more, visit opea.dev.

To keep updated with the OPEA project and participate in technical discussions first-hand, join OPEA’s mailing list and try the project out for yourself. If you have any questions about where to start, reach out to info@opea.dev.

Likewise, you can contribute by visiting and participating in OPEA’s Github page. Check out the GenAiComps and GenAIExamples repositories for a library of microservices components and a collection of generative AI examples respectively.

Follow Arun on X and Linkedin.

Note: Since this interview was recorded, OPEA has grown to more than 40 partners.

The post Arun Gupta on Democratizing Enterprise AI Development appeared first on Semaphore.

View Details

One of the most challenging tasks in writing reliable, consistent, maintainable, and easy-to-read tests is structuring them correctly. Failing to follow a specific structure in each unit test can lead to flaky or unreliable tests—the nightmare of every QA developer.

Over the years, many approaches to unit testing have emerged, with the Arrange-Act-Assert (AAA, or 3A) pattern standing out as the most popular. This pattern involves structuring your unit tests into three steps:

  • Arrange: Set up the test environment.
  • Act: Execute the code to test.
  • Assert: Verify the results.

In this guide, you will learn what the AAA pattern is, how it works, the benefits it offers, and its role in unit test automation.

Let’s dive in!

The AAA Pattern in Unit TestingThe Arrange-Act-Assert pattern, also known as the AAA or 3A pattern, is a widely recognized approach to structuring tests. It was originally proposed by Bill Wake in 2001 and then mentioned in Kent Beck’s influential book “Test Driven Development: By Example” in 2002.

The AAA pattern promotes clarity by recommending that tests be structured into three distinct phases:

  • Arrange everything necessary to perform the test.
  • Act on the target code to be tested by executing it.
  • Assert the expected outcomes.

The AAA pattern enhances readability and maintainability, closely mirroring the Given-When-Then structure developed by Daniel Terhorst-North and Chris Matts as part of BDD (Behavior-Driven Development). Today, nearly all modern unit testing tools with BDD syntax encourage using the AAA pattern.

Time to explore the three phases it consists of!

ArrangeThe Arrange step is where you prepare everything needed for executing the test to make sure it produces accurate results. This involves initializing objects, configuring dependencies, and setting up the environment required for the test scenario.

Examples of operations you may need to perform in this phase include:

  • Creating instances of the classes to be tested
  • Initializing global or local variables with specific values
  • Setting up mock objects to simulate external services
  • Populating a sample database with test data
  • Configuring specific settings and configurations

By thoroughly arranging the test context and state, you ensure that the actions and assertions that follow occur in a controlled and predictable environment. This approach enhances test reliability and reduces the likelihood of flaky tests.

ActThe Act step is where you execute the specific functionality you want to test. This phase involves interacting with the SUT (System Under Test) by calling a method or executing a function that you arranged in the previous step.

In most unit tests, this phase corresponds to calling a function or method of an object/class to perform a specific operation. The goal is to execute the action that will generate the outcome you intend to verify in the next step.

To keep the test easy to read and maintain, this action should be concise and focused. A single line of code where you call the method or function to test is generally enough. This simplicity makes it easier to observe and understand the effect of the specific code under test.

AssertThe Assert step is where you verify that the outcome of the unit test matches your expectations. This phase entails checking the results produced by the Act phase against the expected values to confirm that the SUT behaves as desired.

That typically involves using assertion methods to compare the actual results with the expected ones. Examples of assertions include:

  • Verifying that a method returns the correct value
  • Checking that an object’s state has changed as anticipated
  • Guaranteeing that an exception is thrown under certain conditions

To maintain a test’s readability and effectiveness, the Assert step should be clear and specific. This is why it is recommended to have a single assertion—or a small set of related assertions—per unit test.

Example of a Unit Test Written With the AAA PatternTo better understand how the Arrange-Act-Assert pattern works, take a look at the sample Mocha unit test below organized according to the AAA pattern:

import { expect } from "chai"import { MathUtils } from "src/utils/MathUtils";describe("MathUtils Tests", function () { describe("#getFibonacciNumber()", function () { it("should return the correct Fibonacci number", function () { // Arrange: Initialize the class to test const mathUtils = new MathUtils() // Act: Test the method of a class const result = mathUtils.getFibonacciNumber(6) // Assert: Verify that the method produces the expected outcome expect(result).to.equal(8) }) })}) Now, let’s break down the code to highlight which lines correspond to each of the three phases!

For more complex examples, check this and this Mocha tests on GitHub.

Arrange const mathUtils = new MathUtils() Here, you set up the test by creating an instance of the MathUtils class.

In this example, the Arrange step occurs directly within the it() function. However, when the test requires a more complex setup like a sample database, the phase is often handled in beforeAll() or beforeEach() hooks.

Note that imports and the describe() functions are not considered part of the Arrange phase, which typically involves only the first lines of it() functions and/or the code in beforeAll() and beforeEach() hooks.

Act const result = mathUtils.getFibonacciNumber(6) In this step, you use the mathUtility object you initialized earlier to execute the getFibonacciNumber() method with a given input.

Note: In this example, the input is simple and can be passed directly to the function under test. When dealing with more complex or multiple inputs, you should assign them to dedicated variables in the Arrange step and then use those variables in the Actphase.

Assert expect(result).to.equal(8) Finally, you use the expect() method from Chai to verify that the result from the getFibonacciNumber() method matches the expected output.

Note: In this case, the output is simple and can be asserted directly. For more complex outputs, you should assign them to dedicated variables in the Arrange step and then use them in the Assert step.

7 Good Reasons to Use the AAA PatternExplore the main benefits of adopting the AAA unit testing pattern.

Programming Language and Testing Framework AgnosticLike other software design patterns, the Arrange-Act-Assert pattern is not tied to any specific programming language or tool. That means you can apply it in any testing framework, including Mocha and Jest in JavaScript, JUnit in Java, and pytest in Python.

This consistency allows the development team to use a unified approach to unit testing across different projects, regardless of the testing technology in use.

Better Code OrganizationThe AAA pattern promotes better code organization by encouraging you to separate unit tests into three distinct phases. This fixed structure helps maintain a consistent format across tests referred to different sections of your code.

By isolating the setup, execution, and verification stages, the pattern reduces clutter and confusion. This ensures that each test is focused and well-structured, resulting in more maintainable test code.

Makes Unit Tests Easier to UnderstandThe direct consequences of more organized code are improved readability and enhanced clarity. A unit test written using the Arrange-Act-Assert pattern is inherently straightforward, as it follows three neat steps.

You can easily understand tests following the pattern, even if you are unfamiliar with the specific testing tool or technology used. That is especially true when the chosen testing technology comes with an intuitive assertion API.

Encourages Test-Driven DevelopmentThe AAA unit testing pattern supports TDD (Test-Driven Development) by imposing an explicit structure for your tests. Each unit test that follows the pattern must consist of three steps, regardless of the code being tested.

As a result, you can define the expected behavior before the code is actually implemented. That is in line with TDD principles, where tests are created first to specify the desired functionality. This guides development so that the code meets the specified requirements.

Supports RefactoringThe Arrange-Act-Assert pattern supports refactoring by isolating changes to specific phases of the test, without affecting the overall test structure.

For example, if you modify how a method in a class accepts inputs, you only need to update the Act step. As long as the class uses the same logic to be instantiated and the method produces the same output, the Arrange and Assert phases can remain unchanged.

This allows you to refactor code confidently, knowing that your tests will require minimal updates.

Leads to Separations of ConcernsThe AAA approach to unit testing fosters separation of concerns by ensuring that each of the three phases—Arrange, Act, and Assert—remains focused on its specific task. This clear division makes tests more modular and secure, as each step is responsible for a distinct part of the test process. By keeping setup, execution, and verification separate, you also reduce overlap in test logic.

Respect Testing Best PracticesOver the years, the Arrange-Act-Assert pattern has become a de facto standard in the industry. It is currently mentioned and recommended in several best practices guides, including:

  • “JavaScript Testing Best Practices” on GitHub
  • “Node.js Integration Test Best Practices” on GitHub
  • “Unit Test Basics” in Visual Studio documentation
  • “How To Write a Test” guide on Cypress’ blog
  • Pytest’s documentation

The Role of AAA in Test AutomationThe structured approach of the Arrange-Act-Assert pattern plays a key role in effective unit test automation. Particularly, the Assert step makes it easier to understand test results when executing them in CI/CD pipelines.

In the AAA unit test pattern, each test ends with the Assert phase, which typically includes a single or a few specific assertions. That makes the results of a test easier to evaluate consistently and objectively, even through logs.

For instance, consider the test results image from Mocha’s documentation:

From Mocha’s results, it is evident that the “#indexOf()” test suite in the “Array” collection contains two unit tests. The first fails, while the second passes.

By examining the failing unit test, which follows the AAA pattern, you can quickly pinpoint where to intervene to correct the issue:

describe("Array", function () { describe("#indexOf()", function () { it("should return -1 when not present", function () { // arrange const inputArray = [1, 2, 3] // act const result = inputArray.indexOf(4) // assert expect(result).to.equal(1) }) }) // other test...}) The Arrange step does not contain errors. Similarly, the Act step is implemented as intended. The problem lies in the Assert phase, where the - sign before 1 is missing.

The test asserts the output against a wrong value and fails accordingly. You can fix that with:

expect(result).to.equal(-1) The AAA pattern leads to tests that are straightforward for developers and QA engineers to interpret, even starting from result logs alone. This is essential for swiftly identifying and addressing issues, thereby speeding up the process of fixing broken deployments.

As demonstrated, the AAA pattern facilitates unit test automation. To learn more about this practice, check out our guide on the six principles of test automation.

ConclusionIn this guide, you learned about the Arrange-Act-Assert pattern and what a unit test structured using this approach looks like. You explored each step that the pattern recommends for designing readable and maintainable tests, and the benefits it brings to your testing operations. Backed by the community and widely regarded as a de facto standard, the AAA pattern also proves to be a valuable tool in unit test automation.

The post The Arrange, Act, and Assert (AAA) Pattern in Unit Test Automation appeared first on Semaphore.

View Details

Following on the Role Based Access Control (RBAC) improvements announced in April, we’re rolling out a new feature for Scaleup plans: Custom Roles.

Now, in addition to the default roles, you can create your own with tailored permissions for organizations and projects — giving you the control to decide exactly what actions users can take.

What problem do custom roles solve?Semaphore uses a RBAC model to control what resources and actions users can take. This feature included three default roles for organizations and projects.

While these default roles are enough for many, they cannot fit everyone’s needs. Let’s say you wish to give the finance team access to the Billing page to view spending. Before custom roles, the only way to allow access was using the Admin role, which gave unfettered access to almost everything inside the organization. This is far from ideal from a security perspective.

With the addition of custom roles, you can now create roles to fit the user’s needs. For example, you can create a Finance role that can only view the Billing page and nothing else.

Other use cases for custom roles we envision are:

  • prevent members from viewing or changing secrets
  • allow the compliance team view-only access to the audit logs, members, and group lists
  • provide view-only access to a third party for demo purposes
  • allow the operation team to safely manage infrastructure and self-hosted agents
  • prevent contributors from starting or attaching SSH sessions on CI/CD jobs

So, what are custom roles?Custom roles is a new Scaleup plan feature that allows you to create an unlimited number of new roles. You can then assign the roles to users or groups to precisely manage what resources they can access and actions they can take.

Same as with default roles, there are two kinds of custom roles:

  • Organization roles: Grant actions on the organization such as viewing the dashboards or the audit logs, managing secrets or notifications, and configuring self-hosted agents or pre-flight checks. You’ll find view and manage permissions for most resources on Semaphore.
  • Project roles: Grant actions on projects such as view or manage artifacts, perform job debugging, view secrets, or run tasks, to name a few.

Custom roles improve your security posture in many ways:

  • Flexibility: you can tailor permissions to your specific needs.
  • Security: you can follow the principle of least privilege and lock down user access.
  • Scalability: you can create or modify roles to adapt as your organization grows.
  • Compliance: custom roles let you meet regulatory requirements.

How to create custom rolesInitially, only the organization Owners can create custom roles. You can, however, create a role that can view or manage custom roles.

Organization rolesOrganization roles determine what actions user can take in the organization.

To create an organization-level custom role:

  1. Open the Organization Settings menu
  2. Select Roles
  3. On the Organization Roles section, press New Role

  4. Give a name a description to the new role

  5. Enable the permissions for this role. Use the search box to narrow down options

  6. Optionally, you can grant a project role for all projects in the organization. For example, the role might have view-only access to every project.

  7. Press Save changes

The default roles cannot be modified, but you can see their permissions by pressing the eye button.

Once the role is created, go to People in the organization settings menu, press Change Role, and select the new role. You can only assign one role to an individual or group.

Project rolesProject roles determine what actions users can take within a single project.

To create a project role:

  1. Open the Organization Settings menu
  2. Select Roles
  3. On the Project Roles section, press New Role

  4. Give a name a description to the new role

  5. Enable the permissions allowed to the role. Use the search box to narrow down options

  6. Press Save changes

You can grant project roles by going to the project and selecting the People tab. Press Change Role, and select the new role. You can only assign one role to an individual or group.

ma

FAQWhen will the feature be available?The feature will be generally available on September 30th, 2024.

Help! I can’t find custom rolesThere might be a few reasons why you can’t find the New Role button in the roles setting page:

  • You might not be the Owner of the organization
  • Your organization might not be subscribed to the Scaleup plan

If none of these conditions apply, please get in touch with support@semaphoreci.com so we can help you resolve the issue.

How can I view the available permissions?Open the organization menu and select Settings. Go to the Roles section and press the eye or pencil button next to the role.

You will see the list of all available permissions. The actions enabled for the role are the ones checked.

Why I can’t change default roles?Default roles permissions cannot be changed. This may change in the future, but for now, default role settings are read-only.

How does this impact the GitHub permission integration?The GitHub permission integration is not affected in any way. Users may still gain project-level permissions via their roles in related repositories.

For more information, check the GitHub permissions page.

ConclusionCustom roles give you a new layer of security governance. It is now possible to lock down user permissions very tightly to comply with regulatory requirements. We hope this feature makes user management easier and more secure.

Thank you for reading and happy building!

The post Custom Roles Are Here appeared first on Semaphore.

View Details

In terms of creativity, diversity, and evolution pace, generative AI seems boundless. However, its enterprise implementation remains significantly limited. Businesses are uncertain about making the right choices around AI and struggle to familiarize themselves with the potential of this technology amid cutting through the fragmented offerings from major tech companies. In this episode, Arun Gupta, VP and GM for Open Ecosystem at Intel, tells us about the company’s contributions to the open-source community and shares his experience at the Open Platform for Enterprise AI (OPEA) developing open-platform AI solutions for businesses.

Edited transcription

Arun Gupta’s extensive experience driving cultural change toward open source software includes working at Apple (where he built their first open source program office), Amazon, Red Hat, and Sun Microsystems — and later Oracle, after Sun was acquired. Currently, he is the vice president and general manager of Intel’s Open Ecosystem, and consequently, one of the company’s leading voices on everything open source.

Across industries and computer technologies, companies rely on open-source software and expect it to work flawlessly out of the box. “That’s where Intel engineers go out in these open source communities,” says Arun, explaining that Intel’s involvement in open source is about satisfying its diverse client base across verticals and making sure the features are working as intended.

Contributing to the open-source community is as much part of Arun’s job as part of Intel’s DNA. “We have been the largest corporate contributor to Linux since 2007. We are one of the top contributors to Kubernetes. We participate in 300 plus open source projects: Kubernetes, OpenJDK, PyTorch, TensorFlow, GCC, Clang,” he affirms. Arun also points out that Intel’s participation in “700 plus standard bodies and foundations,” benefits the sustainability of the open-source community as much as the company itself.

Foundational leadership in community projectsAside from code itself, Intel contributes to the sustainability of open source projects largely through foundational leadership. The company has recently launched the Unified Acceleration Foundation, which aims to create an open standard for accelerated computing. Intel is also “a premier member” of the LF AI & Data foundation, and holds a seat on the governing board. “The effort is to build and support an open AI and data community and data-driven open source innovation in the AI and data domains,” Arun explains.

From its leadership position, Arun argues, Intel can “strategically shape the direction and initiatives of the foundation […] and technically, strategically, administratively, financially steer the direction to represent our customers’ and developers’ interest.

Advocating for enterprise AI: The OPEA projectDespite the popularity and growing demand for AI solutions, businesses are still in the early stages of learning how to leverage them. Diving into this new field brings up a lot of inquiries about use cases, security, and the fittest implementation approach. Big tech companies, for their part, are aware of the lack of consensus and unfamiliarity around enterprise AI implementation and want to capitalize on it. “Every hyper scaler is crafting their own bespoke solution and saying, ‘We know it the best’,” says Arun.

To remedy this gap, Arun is currently deeply involved in the Open Platform for Enterprise AI (OPEA) project, an open platform initiative under the LF AI & Data foundation, aimed at simplifying and freeing the development of enterprise AI applications.

As a development framework, OPEA can be used to build gen AI solutions, such as chatbots, providing microservices components like embedding or data storage and the possibility of creating LLMs from your data with improved accuracy using Retrieval Augmented Generation (RAG). As a complementary feature, OPEA offers assessments to evaluate the readiness and trustworthiness of applications and determine at what stage of development they are.

As in the case of AI itself, Arun affirms the project is growing at a marvelous pace: “We launched the project three and a half months ago now. At that time, we had 14 launch partners. We are now up to more than 40 launch partners.” Among these partners are prominent actors such as Canonical, Infosys, Neo4j, Prediction Guard, and Docker, whose interest ranges across different types of AI applications fitting their business use case.

To balance this growth speed and array of contributors, the project has a technical steering committee with members across “a wide range of the community,” says Arun, including a couple from Intel, since “that’s where the largest code contribution is coming in from.” Still, to keep transparency, he keeps his project suggestions and issues in the Github repository, rather than hidden away in “ internal project meetings.” “I just fight the issue on GitHub,” says Arun, “because that’s part of the cultural change that we need to bring, that if the leaders of the project are going to do some sidebar discussions, how would people outside the company understand?

Regarding deployment, developers looking to get started quickly can package OPEA’s components as Docker containers and publish them in Docker Hub. This containerization strategy forms the backbone of OPEA’s deployment flexibility. “As we are building these microservices, using containers as a base layer for the microservices allows us to run it on any cloud-native platform,” Arun explains. Moreover, OPEA’s deployment options extend far beyond. “You could get this up and running on Kubernetes, for example, your EKS or GKE or AKS, doesn’t matter, pick your favorite flavor of Kubernetes, and we can get it up and running over there,” Arun explains.

The bottom lineOPEA is preparing a hackathon and inciting students and professionals worldwide to participate. If you want to be even more involved, OPEA also has working groups dedicated to different areas such as security, end users, and community. To learn more, visit opea.dev.

To keep updated with the OPEA project and participate in technical discussions first-hand, join OPEA’s mailing list and try the project out for yourself. If you have any questions about where to start, reach out to info@opea.dev.

Likewise, you can contribute by visiting and participating in OPEA’s Github page. Check out the GenAiComps and GenAIExamples repositories for a library of microservices components and a collection of generative AI examples respectively.

Follow Arun on X and Linkedin.

Note: Since this interview was recorded, OPEA has grown to more than 40 partners.

The post Arun Gupta on Democratizing Enterprise AI Development appeared first on Semaphore.

View Details

Application programming interfaces (APIs) are the backbone of modern web development, enabling communication between online applications. Not surprisingly, API backends have quickly become the prime target of cyberattacks. Protecting against these threats through robust API security best practices is vital to safeguard your backend applications.

In this article, you will dig into the importance of API threat protection and explore some key techniques for securing your backends.

Let’s dive in!

What Is API Security?API security is the process of protecting APIs against common web attacks, such as data breaches, unauthorized access, and code injection. It focuses on all the practices, strategies, and measures implemented to shield API endpoints and the data they handle from threats and vulnerabilities.

That concept applies to any backend technology, whether it is Node.js, Spring Boot, or Django, and to any types of endpoints, including RESTful, GraphQL, or others. Check out our guide for more guidance on securing Node.js applications in production.

As the great majority of modern web applications rely on APIs, ensuring API security has become a fundamental aspect of the web development process.

Why API Security Is So ImportantAPI security is essential because the web is inherently insecure. As technology evolves, so do the strategies and approaches to exploit it. Considering how advanced malicious bots have become, the number of threats to backend servers has increased significantly in recent years. According to a report from Imperva, nearly one-third of all Internet traffic comes from bad bots. That is alarming!

Common attacks like SQL injection, denial of service, and man-in-the-middle pose serious risks to your API endpoints. If successful, these attacks can result in severe consequences, including:

  • Data leaks: User data and privacy breaches that can lead to legal and regulatory penalties for failing to protect sensitive information. For instance, severe GDPR violations can lead to fines of up to 4% of your global revenue.
  • Reduced service availability: Disruptions and downtime can lead to financial losses, user dissatisfaction, and legal issues in case of SLA breaches.
  • Loss of reputation: Security breaches tarnish your online reputation, triggering negative opinions in the community.

As you can tell, securing a backend is not merely a precautionary measure. Neglecting security can have tangible, detrimental effects on the future of your application and business.

Top 15 API Security Best PracticesNow that you know what API protection is and why it is critical, protect against attacks by applying the API security best practices outlined below

  1. Encrypt Communication via HTTPSBy making sure that your backend is accessible only via HTTPS, you can enhance the confidentiality of data exchanged between clients and your server. This is because HTTPS uses the TLS protocol to create encrypted communication channels, protecting sensitive information like session tokens, passwords, and PII data from eavesdropping and tampering.

Additionally, use Secure cookies as part of this HTTPS-only policy. These special HTTP cookies are sent back to the server by the browser only during HTTPS requests, adding an extra layer of security by ensuring they are transmitted exclusively over encrypted connections.

Investing the time to obtain a legitimate SSL/TLS certificate is also important for enhancing the credibility of your backend. While free options like Let’s Encrypt are available, acquiring a certificate from a trusted CA (Certificate Authority) offers additional validation to users.

  1. Authenticate Your EndpointsAuthenticating the endpoints in your backend is essential to ensure that only trusted users can access them. Popular API authentication methods include:

  2. JWT: JSON Web Tokens provide stateless token-based authentication, allowing secure information exchange between parties.

  3. Basic auth: Using a Base64-encoded username and password in the Authorization header for authenticating server-to-server communication.
  4. API keys: Strings used as unique identifiers to authenticate requests. This technique is commonly adopted in online services providing public endpoints. Effective strategies for API key management involve regularly rotating keys, implementing usage quotas, and monitoring key activity to detect suspicious behavior. Users should also be educated to set API keys only in environment variables and avoid publicly exposing them.
  5. Client SSL certificates: A process where the client and server validate each other’s identities by exchanging SSL certificates. If the validation is successful, they proceed to establish a secure connection. This mutual authentication approach is commonly used in enterprise environments where high security is required for client interactions.

As part of your authentication policies, you also want to enforce strong authentication policies. This means inviting users to set strong passwords and/or rely on MFA (Multi-Factor Authentication) and SSO (Single Sign-On).

  1. Hide Your API Backends Behind a GatewayYour microservice architecture likely consists of several API endpoints spread across different servers. To protect them all consistently, you should place them behind an API gateway.

API gateways centralize incoming traffic, routing client requests to the correct endpoint on the appropriate server. This means exposing your endpoints only through the API gateway, where you can centralize security measures.

This approach is especially important considering that API security best practices continuously evolve. So, threat protection measures require a lot of changes over time. By centralizing security in an API gateway, maintenance becomes easier and you do not have to keep updating single endpoints.

Also, do not forget that the most secure API backend is one that is not even exposed to the public Internet but is only accessible through a private network. Implementing this involves placing your APIs behind a bastion host or a VPN, giving access only to authorized users who can tunnel into the private network.

  1. Enforce Fine-Grained Access Control StrategiesFine-grained access control (FGAC) is an authorization strategy to allow or deny access to data and resources based on multiple aspects. In particular, it involves granting specific permissions to users based on operations they need to perform on selected resources.

FGAC follows the principle of least privilege, making sure that each user has the minimum required permissions to do what they need to do. This reduces the risk of unauthorized operations and data breaches.

Popular fine-grained access control strategies are:

  • RBAC (Role-Based Access Control): Assigning permissions to users based on roles (e.g., admins, editors, visitors, etc.)
  • ABAC (Attribute-Based Access Control): Granting access based on user attributes and context (e.g., device, time, location, etc.).

  • Implement API Rate LimitingAPI rate limiting is a protection technique that restricts the number of requests a user can make within a specific timeframe. This involves setting constraints on incoming requests to mitigate threats such as DDoS attacks, brute force attempts, and scraping bots.

Two common techniques to implement rate limiting are:

  • Block requests: Automatically block any requests that exceed the defined rate limits, returning a 429 Too Many Requestserror.
  • Delay requests: Introduce a delay before responding to requests that exceed the limits, slowing down the requester.

By limiting the request rate, you can avoid server overloads and ensure fair usage to all users.

  1. Limit the Size of Incoming RequestsMost backend servers, frameworks, and libraries have a default request body size limit set to a few megabytes. This can become an issue during request flooding attacks. The problem is that parsing incoming requests takes time and resources, which increases as the request body becomes larger.

To prevent your server from being overwhelmed by large requests, set a size limit for incoming requests. This setting can typically be configured at both the server level (e.g., using the LimitRequestBody directive in Apache) and the application level (e.g., by limiting the size of the incoming request body).

  1. Use Standard Error MessagesEach HTTP error code is associated with a standard name, such as “Not Found” for 404. Since these names are concise and not very descriptive, you may be tempted to include longer, more detailed messages in your error responses. Longer messages will help the client better handle the error, but they may also reveal useful information to malicious users.

As a rule of thumb, avoid detailed error responses including stack traces or technical reasons behind errors. This information does not add real value to the client and will only make it easier for attackers to exploit vulnerabilities in your backend. For enforcing API security, opt instead for generic and standardized error messages.

  1. Remove the Default Framework HeadersPopular backend technologies often set default HTTP headers in their responses. For example, Express.js and others automatically set the X-Powered-By header with the framework’s name and sometimes even its version.

These default headers expose the underlying technology stack, helping attackers target known vulnerabilities. As a fundamental API threat protection measure, remove these headers to reduce your backend’s attack surface and reduce potential risks.

  1. Validate and Sanitize User InputInput validation ensures that the data received by endpoints adheres to specific formats and respects expected constraints. This API security best practice reduces errors in the business logic of your backend and mitigates XSS (Cross-Site Scripting) attacks.

Meanwhile, input sanitization focuses on removing potentially harmful characters from input data. That protects against popular code injection attacks like SQL injection.

  1. Set the Security HTTP HeadersSecurity HTTP headers are a set of HTTP headers designed to add an additional layer of protection against common web vulnerabilities. These headers include Referrer-Policy, Content-Security-Policy, X-Content-Type-Options, and others. You can check whether your backend sets them correctly at the Security Headers site.

For example, defining a CSP policy through the Content-Security-Policy allows you to specify which trusted sources the browser can load resources from. That mitigates XSS attacks and other common threats.

  1. Integrate Logging and Application MonitoringWhen something goes wrong, you need to understand when, what, why, and how it happened. This is exactly what logging is all about. By tracking your backend activities in log text files, you can reconstruct events in case of errors to address them more easily.

Similarly, application performance monitoring (APM) involves tracking the performance and health of software. Integrate an APM library into your backend, and this will start sending useful data in the background using standard protocols like OpenTelemetry. That data can then be used to get real-time insights and set alerts for unusual behavior, helping immediately identify suspicious activities and security breaches.

  1. Regularly Perform Audits, Penetration Tests, and Stress TestsA code audit for security involves examining server configurations and the codebase of your backend to ensure compliance with security standards. Instead, penetration tests are more proactive and involve simulating real attacks to identify potential entry points for attackers. Finally, stress tests evaluate API performance under high loads, ensuring resilience against denial-of-service attacks and automated bots.

Regularly conducting code audits, penetration tests, and stress tests helps discover weaknesses in your backend and address them before malicious attackers can take advantage of them.

  1. Use Security Linters, SCA, and SAST ToolsSecurity linter plugins, such as eslint-plugin-security, analyze your code for known vulnerabilities and adherence to API security best practices. In detail, they flag potential issues like unsafe regular expressions or dangerous import instructions.

SCA (Software Composition Analysis) tools evaluate the dependencies used in your application for known vulnerabilities. They identify outdated or insecure components and recommend updates or replacements to keep your software stack secure. Tools like Snyk and Black Duck are popular for performing these analyses. In general, you should always keep your third-party libraries up-to-date, relying on such a service to alert you when known security issues emerge.

SAST (Static Application Security Testing) tools perform comprehensive scans of your codebase to identify security flaws in the logic or architecture of your backend. Examples of SAST tools are SonarQube and Lacework.

By integrating such security tools into your development workflow, you can proactively prevent security risks.

  1. Use API FirewallsAPI firewalls monitor and filter both incoming and outgoing traffic for malicious activity. When it comes to backend protection, all-in-one web application firewall (WAF) solutions like Cloudflare, AWS WAF, and Akamai have become particularly popular.

WAFs provide comprehensive security by blocking known threats, preventing unauthorized access, and mitigating known attacks such as SQL injection and cross-site scripting. In addition, they generally offer features like rate limiting and bot protection to further enhance the security of your APIs.

  1. Follow Established Security StandardsAPI security is a cat-and-mouse game, with new threats emerging daily and constant updates to security systems to protect against them. This is why staying abreast of the latest developments is imperative!

Adhering to trusted sources like OWASP offers invaluable guidance, helping you keep your backend robust and up-to-date in the face of evolving threats. Specifically, the OWASP Top Ten project provides crucial insights into the most prevalent web threats.

ConclusionIn this guide, you learned how to safeguard backend applications through the adoption of API security best practices. As a developer, it is your responsibility to maintain application functionality, safeguard your users, and prevent data breaches. By incorporating these techniques, you can bolster the resilience of your backend against common web attacks, effectively mitigating risks and safeguarding the security of your application.

The post API Security Best Practices: Protecting Your Data in Transit appeared first on Semaphore.

View Details

Assistive coding is one of the most powerful ways to apply large language models (LLMs). A well-trained model integrated into your development environment can supercharge your productivity.

Hosted models as-a-service have become cheaper and more effective as their adoption accelerates. However, there are cases in which you may want to run your own:

  • Privacy: Relying on hosted APIs requires sending your data to a third party to populate context.
  • Cost: Iterating often with a large codebase can incur charges quickly and discourage experimenting with different prompts.
  • New developments: Models with significant new capabilities appear on an almost weekly (or daily!) basis. Running your own open models can keep you ahead of the curve.

This article will walk you through how to leverage the right self-hosted LLM and integrate it into your environment.

Doing it Yourself: An OverviewThe most popular and effective commercial LLM coding assistant services pair highly-trained models with powerful hardware to deliver accurate suggestions quickly. To build your own, you’ll need the same: a refined model well-suited for coding tasks, compute hardware with sufficient power to generate tokens (or suggestion output) quickly, and an editor or IDE integration to bring it all together.

In this guide we’ll use Ollama to easily run our desired models and assume that you have a functional ollama service API available for use. If not, many resources are available to help you get started with Ollama. Consider using the official quick start documentation as a starting point.

The primary setup considerations to bear in mind in the context of this article are:

  • More capable models need significant resources. If you intend to run models beyond 40 billion parameters, you likely need powerful local hardware (or optimized cloud resources).
  • Choosing the right model is as much an art as it is a science. This tutorial will help explore models and their capabilities, but experimenting with different models against your use case will help you select the most effective strategy.

We’ll look at editor integrations for VSCode, Vim, and Emacs — though most editor ecosystems provide some method of integration with LLM APIs. When evaluating an LLM integration, API compatibility against Ollama is the primary requirement. The models themselves can be swapped out from underneath the Ollama service.

To ensure a fair comparison across both models and editor integrations, we’ll compare models with similarly-sized parameters, then integrations, and finally demonstrate the two paired together.

A Brief Tour of Coding ModelsBefore diving into code-optimized LLMs, we should establish a baseline prompt to gauge their effectiveness against a uniform set of requirements.

Consider the following example:

Write a Python script that uses the click library to accept a URL argument that the script will POST input to with the request body populated from stdin and then pretty print the resulting response body. Print a friendly message if the response returns an HTTP error status code. Do not provide commentary or any other explanatory text, return code only.

This prompt judges a model’s ability to:

  • Use standard libraries (to read input from standard input)
  • Use third-party modules (with the click library)
  • Write functions that accept and use arguments
  • Perform network HTTP operations
  • Present user-readable output
  • Implement error handling

To follow along, the only prerequisite is a functional Ollama server accessible from your own machine with an ollama run command. If you’re running Ollama on your local machine, then ollama commands should default to localhost as the API endpoint. If you’re running Ollama on a cloud resource or other remote machine, then invoke your commands in the form of:

OLLAMA_HOST=ollama-host:11434 ollama run Where ollama-host is the destination hostname or IP address of the remote server. The examples that follow are results derived from entering our test instructions at the > prompt from the ollama run command.

With this criteria in hand, let’s explore!

codeqwencodeqwen is a coding-optimized model with a parameter size small enough at 7B parameters that it can fit into the available memory space of consumer-grade GPUs. Given our baseline Python program prompt, here’s an example response from codeqwen:

import clickimport requestsimport json@click.command()@click.argument('url')def post_request(url): try: response = requests.post(url, data=input()) response.raise_for_status() # Raise an HTTPError for 4xx/5xx status codes print(json.dumps(json.loads(response.text), indent=2)) # Pretty print the response body except requests.HTTPError as http_err: print('HTTP error occurred:', http_err) # Print HTTP error message except Exception as err: print('An error occurred:', err) # Print other errorsif __name__ == '__main__': post_request() Despite its relatively lightweight footprint, codeqwen does an excellent job at fulfilling our prompt, honoring our requirements with good patterns:

  • It leverages the requests library without any hints to do so.
  • Error handling catches both HTTP errors and other exceptions.
  • The standard json library pretty-prints responses (with indent=2).

The source format is suitable for use as a script and meets the requirements defined in our baseline prompt.

deepseek-coderThe deepseek-coder model has sizes similar to those available to codeqwen, ranging from 1B to 33B. Although a v2 deepseek-code model exists, we’ll contrast the 6.7B deepseek-coder model against codeqwen:7B to compare across similar parameter sizes.

Given the same initial prompt, deepseek-coder:6.7B returns the following Python:

import clickimport requestsfrom pprint import pformat@click.command()@click.argument('url')def post_request(url): try: resp = requests.post(url, data=click.getchar()) if 400 <= resp.status_code < 600: click.echo("An HTTP error {} occurred.".format(resp.status_code), err=True) else: click.echo(pformat(resp.json())) except requests.exceptions.RequestException as e: click.echo('Error: {}'.format(e), err=True)if __name__ == '__main__': post_request() Like codeqwen:7B, deepseek-coder:6.7B correctly uses the chosen libraries and infers the use of requests as an HTTP client. Some noteworthy differences include:

  • Using the native pprint library to pretty-print JSON parsed from the .json() requests method.
  • Manually checking that the response code falls within a successful range. This is slightly less ergonomic than codeqwen’s .raise_for_status() approach.
  • The click.getchar() function only accepts one character, and does not read from standard input.

This solution is mostly functional but misses subtle success criteria from our original requirements. For example, it cannot accept an entire payload from standard input rather than one character from an interactive prompt.

codellamacodellama is a Llama 2-based model from Meta tuned for coding and available in many different parameter sizes including 7B, 13B, 34B, and 70B. We’ll use the 7B model to compare model capabilities as with codeqwen and deepseek-coder.

import clickimport requestsfrom rich.console import Console@click.command()@click.argument("url")def main(url: str): console = Console() try: response = requests.post(url, data=console.input()) if response.status_code == 200: console.print(response.json()) else: console.print("Error: {}".format(response.status_code)) except Exception as e: console.print("An error occurred: {}".format(e)) Once again, the differences between this solution and the previous attempts are interesting to explore:

  • The use of type annotations in main() is an optional but generally good practice to improve the readability of the code.
  • The rich library accepts input and pretty-prints the response which is an effective way to repurpose the library to serve two purposes. The console.input() method also properly accepts input via either stdin or command line input.
  • Without a call to main() we’ll need to either invoke it manually or wrap it with a standard conditional check against __name__ == "main".

While missing the trailing __name__ check at the end of the generated code, the use of features like type annotations and additional libraries suggests that codellama may be well-suited for code that follows best practices. Larger parameter sizes also offer the potential for more comprehensive code completion.

llama3.1Although not specifically tuned for coding, the llama3.1 model is a recent addition to the family of open-source models. With parameter weights ranging from 8B to 405B, it can compete with even the most capable commercial LLM offerings.

Queried against our baseline prompt, the 8B model returns the following:

import clickimport jsonfrom requests import postfrom sys import stdin@click.command()@click.argument('url')def main(url): """POST input to URL and print response""" req = post(url) print(json.dumps(req.json(), indent=4)) if req.status_code < 400: return click.echo(f"Error {req.status_code}: {req.reason}", err=True)if __name__ == '__main__': main() Despite being a generalized model, llama3.1 performs relatively well against models trained specifically for coding tasks:

  • The requests library is the only other additional dependency besides click.
  • Unlike every other generated response, this function includes a document string.
  • The code snippet is complete with a trailing conditional to run as a script when invoked from the command line.

However, the generated code has a glaring bug: it never accepts any input! Despite importing stdin, the call to requests.post never includes any request body. While the solution is well-formatted and runs, it misses our requirement for payload input entirely.

In an environment with sufficient computing power, llama3.1 may still be worth exploring. Models with more parameters are more capable than most of the other models we’ve explored so far.

Editor IntegrationIntegrating your chosen model into an editor is a key step to making full use of an LLM. Both chat-based assistive coding and autocomplete-style generation can significantly improve their utility.

The Ollama API abstraction layer provides a uniform access method to different coding models, which makes editor integration simpler. Even across different coding assistant extensions or packages, as long as each is compatible with the Ollama API, the backend can remain the same without any significant friction.

Let’s look at integrations that can present a convenient interface in a variety of editors.

VSCodeAmong the Ollama-compatible extensions available on the VSCode Extension Marketplace, we’ll use Ollama Autocoder as an illustrative case for integrating an Ollama model into VSCode.

After installing the extension, open the settings for the extension:

Scroll to the Endpoint setting and change it to the Ollama API endpoint for your running service. For example, in this screenshot, replace ollama-host with either your Ollama endpoint IP address or hostname:

After making these changes, you’re ready to try Ollama-powered autocompletion!

Open a Python source code file and begin with an appropriate script shebang like #!/usr/bin/env python. Follow it with a comment including our test prompt, and position the cursor at the end of the file. The extension will pick up at the cursor location with text preceding as context to the model. Use the command palette (Ctrl-Shift-P) to execute the command Autocomplete with Ollama. The extension should stream generated code:

The preceding recording is generating code from the codeqwen model running on a legacy NVidia 1070 GTX GPU.

EmacsPackages like gptel and org-ai offer integration with LLM-based APIs, but this example will use ellama as a simple example of code completion with Ollama.

The following elisp snippet uses the use-package macro to install and configure the ellama package and set its provider backend to an ollama API at http://ollama-host:14434:

(use-package ellama :config (require 'llm-ollama) (setq ellama-provider'(make-llm-ollama :host "ollama-host" :port 11434 :chat-model "codeqwen" :embedding-model "codeqwen"))) Reload Emacs after placing the code snippet into ~/.emacs.d/init.el or evaluate the use-package form. Position the point in a source code file where you’d like to generate code and invoke M-x ellama-code-complete. The following recording demonstrates this in a Python source file preceded by our baseline prompt:

As with the VSCode extension, ellama will rely on the Ollama API endpoint to generate code based on the editor context. Completion input streams into the open buffer until it finishes.

NeovimThe Neovim plugin model.nvim offers the ability to integrate with APIs like Ollama and provide streaming completion as with the integrations with VSCode and Emacs.

To install and configure model.nvim with the lazy.nvim plugin manager, use the following configuration under the section for require("lazy"). This will also prepare the plugin to use the hosted Ollama API endpoint:

require("lazy").setup({ spec = { "gsuuon/model.nvim", config = function() local ollama = require('model.providers.ollama') require('model').setup({ prompts = { ['ollama:codeqwen'] = { provider = ollama, options = { url = "http://ollama-host:11434", }, params = { model = "codeqwen", }, builder = function(input) return { prompt = '<|im_start|>' .. input .. '<|im_stop|><|im_start|>assistant', stops = { '<|im_stop|>' }, } end, } }, }) end, },}) The ['ollama:codeqwen'] prompt stores the configuration for our ollama prompt. Change the url option to point at your ollama API endpoint, listed in this example as ollama-host.

Start Neovim with nvim and open a new Python source buffer with :e main.py and populate the buffer with the baseline prompt. Move the cursor to a newline and invoke the command :Model ollama:codeqwen. The plugin will stream the response into the buffer to complete the code:

Edit the plugin configuration as necessary to define additional models to use with the :Model command to use other models.

SummaryIn this guide, we have:

  • Evaluated a variety of different large language models and their effectiveness at fulfilling coding instructions.
  • Showcased editor extensions for VSCode, Emacs, and Neovim that integrate with ollama APIs.
  • Combined large language models that fit on consumer-grade hardware with those extensions to stream generated code completion.

To continue exploring self-hosted modules:

  • Browse the library of Ollama models.
  • Install and use Ollama integrations.
  • Explore tools optimized for local hosting like llama.cpp and exo.

The post A Guide to Self-Hosted LLM Coding Assistants appeared first on Semaphore.

View Details

Starting October 1st, 2024, Semaphore will remove the Xcode 14 image from the available machine pool. This change affects all Apple developers who are building their projects with Xcode 14.

Apple has announced that as of April 2024, all iOS and iPadOS apps submitted to the App Store must be built with a minimum of Xcode 15 and the iOS 17 SDK

How do I know if I am impacted by this change?If your organization builds projects on Apple machines, you might be affected. Only projects using the Xcode 14 image will be affected by this change.

You can check what image your projects are using with the Workflow Editor.

To see what image you are using follow these steps:

  1. Check the settings for all your pipelines. If the OS Image is “macos-xcode14” you are affected

  2. Open all your blocks. If the Override global agent definition is enabled and the OS Image is “macos-xcode14” you are affected.

  3. Alternatively, you can search for the text "macos-xcode14" in your pipeline YAML files inside the .semaphore folder. If you find this string, you are affected,

How to prepare for the change?If your organization is affected by the deprecation, we encourage you to switch to the Xcode 15 image before the deprecation date.

You can change the macOS image in the Workflow Editor.

Or edit your pipeline YAML files to replace all instances of "macos-xcode14" with "macos-xcode15". For example:

version: v1.0name: Initial Pipelineagent: machine: type: a1-standard-4 os_image: macos-xcode15blocks: - name: 'Block #1' task: jobs: - name: 'Job #1' commands: - checkout We encourage you to test your workflows with the Xcode 15 image thoroughly before making new releases with the newer image.

Feel free to reach out to us at support@semaphoreci.com if you have any questions or need our assistance. We’ll be happy to help.

The post Xcode 14 Deprecation in Semaphore appeared first on Semaphore.

View Details

Access control is essential for application security. It ensures that only authorized users can access specific resources or perform certain actions. Effective access control prevents unauthorized access, safeguards sensitive data, and maintains application integrity.

In the realm of microservices architectures, managing authorization logic across various services can become a complex task. Cerbos offers a solution by centralizing authorization policies. This simplifies the process because you can make decisions from any part of the application. You can avoid a maintenance burden as your application evolves.

In this tutorial, we will explore Cerbos, a modern, open-source solution, for managing and enforcing access control policies. While popular tools like Okta excel at user authentication and basic access control, applications with complex permission structures require a more granular approach. Cerbos offers a flexible method for defining and applying detailed access control rules. It supports various frameworks and languages, such as Python and Java.

In this article, you will learn:

  • How Cerbos works and what benefits it offers.
  • How to secure a Spring Boot application using Cerbos policies.
  • How to test Cerbos policies in the Semaphore CI pipeline.

PrerequisitesTo ensure smooth learning, this tutorial assumes you have:

  • Basic Docker knowledge, including containers and Docker Compose.
  • Fundamentals of Spring Boot, such as working with JPA entities and REST APIs.
  • An integrated development environment (IDE) like IntelliJ IDEA.

Let’s get started!

Understanding CerbosThe core concept of Cerbos is to define all access control rules in a central location in a human-readable format. When you update these rules, Cerbos applies the changes across all instances serving your application. This way, you don’t need to release any code changes.

Cerbos consists of two main components:

  • Cerbos Hub: A managed platform for creating and managing policies collaboratively.
  • Cerbos Policy Decision Point (PDP): An open-source engine that enforces these policies.

Benefits of Using Cerbos* Developers can concentrate on building core application features while delegating access rule management to product owners or security teams. * You can deploy Cerbos as a separate microservice. * Cerbos makes authorization logic easier. It replaces complex, hard-coded permission checks with a single call to the Cerbos policy engine.

For example, look at this Java boiler-plate code:

With Cerbos, you can simplify it to the absolute minimum:

What Are Cerbos PoliciesYou can define Cerbos policies in YAML or JSON files. These policies consist of two main parts:

  • Resources: Specify an entity within your application that requires protection through access rules. For example, an employee record with personal details, salary, etc.
  • Rules: Determine who can access each resource and what actions they can perform. For instance, colleagues from the HR department can access and manage employee records.

Cerbos provides several ways to create policies:

  • Cerbos playground: An interactive interface for defining and testing policies.
  • Cerbos Hub: A managed platform for policy creation and management.
  • Manual Configuration: Defining policies directly in YAML files.

In this tutorial, we will use the Cerbos Playground to define our policies.

Integrating Cerbos into a Spring Boot ApplicationProject ScenarioIn our demo application, we will define access control policies for an employee management system. The system has two primary roles: regular employees and HR personnel. The policies will ensure that regular employees can only view their own profiles. HR personnel will have full control over all employee records.

Prepare the ProjectCreate a new Maven project in your favorite IDE. For example, name it CerbosSpringBootDemo.

Create the Cerbos policiesVisit the Cerbos playground.

Create two custom roles: employee and hr. The resource we will manage is the employee’s profile.

  • The employee role has read-only access to the profile.
  • The hr role has full access to perform all actions.

Finally, press Generate.

You can then see what the policies look like:

On the left panel, you can see the YAML files that contain the policies.

We are interested in the resource_policies folder. Let’s copy the contents of these files and save them to our local machine.

Create a new folder in the root of your project called cerbos-policies. The name can be different but note that Cerbos will look for a folder called policies when running in the container. Create a sub-folder called testdata. Make sure you have the following structure:

├── cerbos-policies│ ├── profile_test.yml│ ├── profile.yaml│ └── testdata│ ├── principals.yaml│ └── resources.yaml * principals.yaml:

principals: employee#1: id: employee#1 roles: - employee attr: {} hr#2: id: hr#2 roles: - hr attr: {} This file defines the principals (users) for the policy tests, specifying their IDs, roles, and attributes.

  • resources.yaml:

resources: profile#1: id: profile#1 kind: profile attr: {} This file defines the resources that will be subject to the policies being tested.

  • profile.yaml:

apiVersion: api.cerbos.dev/v1resourcePolicy: resource: profile version: default rules: - actions: - create effect: EFFECT_ALLOW roles: - user - admin - hr - actions: - read effect: EFFECT_ALLOW roles: - user - admin - employee - hr - actions: - update effect: EFFECT_ALLOW roles: - user - admin - hr - actions: - delete effect: EFFECT_ALLOW roles: - admin - hr This policy file specifies the resource as the profile and sets rules that allow an employee with the hr role to perform all actions. It restricts employees with the employee role to read their own profiles only.

  • profile_test.yaml:

name: profileTestSuitedescription: Tests for verifying the profile resource policytests:- name: profile actions input: principals: - employee#1 - hr#2 resources: - profile#1 actions: - create - read - update - delete expected: - resource: profile#1 principal: employee#1 actions: create: EFFECT_DENY read: EFFECT_ALLOW update: EFFECT_DENY delete: EFFECT_DENY - resource: profile#1 principal: hr#2 actions: create: EFFECT_ALLOW read: EFFECT_ALLOW update: EFFECT_ALLOW delete: EFFECT_ALLOW This YAML file defines a set of tests for verifying the access control policy related to the profile resource. It specifies the actions that different principals (users with roles) can perform on the resource and what the expected outcomes should be. Note that Cerbos expects the file suffix _test.

Prepare the Infrastructure for Local DevelopmentYou can integrate Cerbos into your stack in two main ways:

  • By downloading and installing the binaries.
  • Using a docker container.

We will pull the official Docker image and run it via docker-compose. We will also deploy our custom policies to the running Cerbos PDP instance.

Create a docker-compose.yml file in the root of your project with the following content:

version: "2.1"services: my-cerbos-container: container_name: my-cerbos-container image: ghcr.io/cerbos/cerbos:0.34.0 ports: - "3592:3592" - "3593:3593" volumes: - ./cerbos-policies:/policies expose: - "3592" - "3593" command: compile /policies Let’s break down the docker-compose.yml file to understand what each section does:

  • image: Specifies the Docker image for this service, which is the Cerbos PDP version 0.34.0 from GitHub Container Registry.
  • container_name: Names the container my-cerbos-container for easy identification.
  • ports: Maps ports 3592 and 3593 on the host machine to the same ports in the container, allowing external access.
  • expose: Indicates that ports 3592 and 3593 should be exposed, making them available for linked services.
  • volumes: Mounts the ./cerbos-policies directory from the host machine to the /policies directory in the container, providing the Cerbos PDP access to your policy files.
  • command: Executes the Cerbos command compile /policies to run the tests in the profile_test.yaml file.

Create the Spring Boot ApplicationAdd the following dependencies to the parent pom.xml file:

<dependencies> <!-- Spring boot dependencies --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>3.2.3</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> <version>3.2.3</version> </dependency> <dependency> <groupId>jakarta.persistence</groupId> <artifactId>jakarta.persistence-api</artifactId> <version>3.1.0</version> </dependency> <!-- Database --> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>2.2.224</version> </dependency> <!-- Cerbos dependencies --> <dependency> <groupId>dev.cerbos</groupId> <artifactId>cerbos-sdk-java</artifactId> <version>0.12.0</version> </dependency> <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-core</artifactId> <version>1.64.0</version> </dependency> <!-- Other dev tools --> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.5.6</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.30</version> </dependency> </dependencies> Below is a summary of each dependency:

  • spring-boot-starter-web: Provides the core web functionalities to build a Spring Boot web application.
  • spring-boot-starter-data-jpa: Adds support for JPA, enabling easy database interaction with repositories.
  • jakarta. persistence-api: Supplies the Jakarta Persistence API which is essential for object-relational mapping and managing relational data in Java applications.
  • h2: Includes the H2 database engine, a lightweight in-memory database useful for development, testing, and small-scale applications.
  • cerbos-sdk-java: Provides the Cerbos SDK for Java, which facilitates integration with the Cerbos authorization system to handle access control policies.
  • grpc-core: Supports the core functionality for gRPC (Google Remote Procedure Call), which Cerbos uses for communication.
  • logback-classic: Implements logging capabilities using Logback.
  • lombok: Offers annotations to reduce boilerplate code in Java, such as generating getters, setters, and constructors automatically.

Configure the application.yml file:

spring: datasource: url: jdbc:h2:mem:testdb driverClassName: org.h2.Driver h2: console: enabled: true jpa: database-platform: org.hibernate.dialect.H2Dialect hibernate: ddl-auto: create show-sql: truelogging: level: root: warn org.cerbos.demo: debug Key points:

  • ddl-auto: create: Configures Hibernate to create the database schema and tables at startup. This is helpful for development and testing but should be changed for production.
  • h2:console:enabled:true: Enables the H2 console. You can access it at localhost:8080/h2-console when the application starts.
  • show-sql: true: Enables the logging of SQL statements generated by Hibernate.
  • logging.level: Reduces the root level output and sets only the necessary org.cerbos.demo package to debug.

Let’s configure the Cerbos client. Create a new class CerbosConfig.java:

@Configurationpublic class CerbosConfig { @Bean public CerbosBlockingClient cerbosClient() throws CerbosClientBuilder.InvalidClientConfigurationException { LoadBalancerRegistry.getDefaultRegistry().register(new io.grpc.internal.PickFirstLoadBalancerProvider()); NameResolverRegistry.getDefaultRegistry().register(new io.grpc.internal.DnsNameResolverProvider()); return new CerbosClientBuilder("localhost:3593").withPlaintext().buildBlockingClient(); }} This configuration sets up and registers a CerbosBlockingClient bean in the Spring application context. This allows the application to connect to the PDP to check access control policies. It uses load balancer and name resolver registrations to ensure the gRPC client can find and connect to the PDP server.

You can find more details and custom options for the CerbosClient in the Cerbos-Java GitHub repository.

Let’s create the model for our application. Create a new class Employee.java that represents the employees table:

@Entity@Table(name = "employees")@Datapublic class Employee { @Id @Column(name = "employee_id" , nullable = false) private Long employeeId; @Column(nullable = false) private String name; @Column(nullable = false, unique = true) private String email; @Column(nullable = false) private Double salary; @Column(nullable = false) private String role;} This entity contains usual employee information.

Create the Profile.java that represents the profiles table:

@Entity@Table(name = "profiles")@Datapublic class Profile { @Id private Long id; @OneToOne(cascade = CascadeType.PERSIST) @JoinColumn(name = "employee_id") private Employee employee;} This entity contains the employee details.

Key points:

@OneToOne(cascade = CascadeType.PERSIST): Defines a one-to-one relationship between the Profile and Employee entities. The cascade = CascadeType.PERSIST attribute means that when you persist a Profile entity, you also persist the associated Employeeentity.

Let’s create the JPA repositories: EmployeeRepository.java and ProfileRepository.java. They will allow access to the entities.

public interface EmployeeRepository extends JpaRepository<Employee, Long> { }

public interface ProfileRepository extends JpaRepository<Profile, Long> { } We will build the backend of the Spring boot application and test the endpoints by sending cURL requests.

Create a ProfileController.java for the REST communication:

@RestController@RequestMapping("/api/profile")@RequiredArgsConstructor@Slf4jpublic class ProfileController { private final EmployeeRepository employeeRepository; private final ProfileRepository profileRepository; private final CerbosBlockingClient cerbosBlockingClient; @GetMapping("/get/{profileId}/{employeeId}") public ResponseEntity<String> getProfile(@PathVariable String profileId, @PathVariable String employeeId) { Profile profile = getProfileOrNotFound(profileId); if (profile == null) { return handleProfileNotFound(); } Employee employee = getEmployeeOrNotFound(employeeId); if (employee == null) { return handleEmployeeNotFound(); } Principal principal = Principal.newInstance(employeeId, employee.getRole()) .withAttribute("id", AttributeValue.stringValue(employeeId)); Resource resource = Resource.newInstance("profile", profileId) .withAttribute("owner", AttributeValue.stringValue(String.valueOf(profile.getEmployee().getEmployeeId()))); if (!isAllowed("read", principal, resource)) { log.debug("Not allowed to read profile"); return handleForbidden(); } return ResponseEntity.ok().body(profile.getEmployee().toString()); } @DeleteMapping("/delete/{profileId}/{employeeId}") public ResponseEntity<String> deleteProfile(@PathVariable String profileId, @PathVariable String employeeId) { Profile profile = getProfileOrNotFound(profileId); if (profile == null) { return handleProfileNotFound(); } Employee employee = getEmployeeOrNotFound(employeeId); if (employee == null) { return handleEmployeeNotFound(); } Principal principal = Principal.newInstance(employeeId, employee.getRole()); Resource resource = Resource.newInstance("profile", profileId); if (!isAllowed("delete", principal, resource)) { log.debug("Not allowed to delete profile"); return handleForbidden(); } try { profileRepository.deleteById(Long.valueOf(profileId)); boolean isDeleted = profileRepository.findById(Long.valueOf(profileId)).isEmpty(); return isDeleted ? ResponseEntity.ok().body("Profile deleted successfully") : ResponseEntity.internalServerError().body("Failed to delete profile"); } catch (Exception e) { return handleInternalServerError(e); } } private boolean isAllowed(String action, Principal principal, Resource resource) { return cerbosBlockingClient.check(principal, resource, action).isAllowed(action); } private Profile getProfileOrNotFound(String profileId) { return profileRepository.findById(Long.parseLong(profileId)).orElse(null); } private Employee getEmployeeOrNotFound(String employeeId) { return employeeRepository.findById(Long.parseLong(employeeId)).orElse(null); } private ResponseEntity<String> handleProfileNotFound() { return ResponseEntity.badRequest().body("Profile not found"); } private ResponseEntity<String> handleEmployeeNotFound() { return ResponseEntity.badRequest().body("Employee not found"); } private ResponseEntity<String> handleForbidden() { return ResponseEntity.status(403).body("Forbidden"); } private ResponseEntity<String> handleInternalServerError(Exception e) { log.error("Error processing request: ", e); return ResponseEntity.internalServerError().body("Error processing request: " + e.getMessage()); }} Key aspects and functionality:

The controller fetches data from the JPA repositories. It performs authorization checks using Cerbos and handles both successful and failed operations.

`GET /api/profile/get/{profileId}/{employeeId}:` * Creates Principal and Resource objects to represent the current user and the profile resource. * Uses cerbosBlockingClient to check if the user is allowed to perform the “read” action on the resource. * Returns 403 Forbidden if the access is denied, otherwise returns the profile details.

`DELETE /api/profile/get/{profileId}/{employeeId}:` * Performs the same steps as above, but this time it checks if the user can delete the profile.

Finally, let’s create the Main.java class:

@SpringBootConfiguration@ComponentScan(basePackages = "org.cerbos.demo")@EnableJpaRepositories@EnableAutoConfiguration@Slf4jpublic class Main { static ConfigurableApplicationContext appCtx; public static void main(String[] args) { var app = new SpringApplication(Main.class); appCtx = app.run(args); } @Bean CommandLineRunner commandLineRunner(EmployeeRepository employeeRepository, ProfileRepository profileRepository) { return args -> { populateDb(employeeRepository, profileRepository); }; } void populateDb(EmployeeRepository employeeRepository, ProfileRepository profileRepository) { Employee employee1 = new Employee(); employee1.setEmployeeId(123L); employee1.setName("John Doe"); employee1.setEmail("john.doe@me.com"); employee1.setRole("employee"); employee1.setSalary(1500.0); Employee employee2 = new Employee(); employee2.setEmployeeId(321L); employee2.setName("Marie Smith"); employee2.setEmail("marie.smith@me.com"); employee2.setRole("employee"); employee2.setSalary(2000.0); Employee hr = new Employee(); hr.setEmployeeId(456L); hr.setName("Andrew Anderson"); hr.setEmail("andrew.anderson@me.com"); hr.setRole("hr"); hr.setSalary(1000.0); employeeRepository.save(employee1); employeeRepository.save(employee2); employeeRepository.save(hr); Profile profile = new Profile(); profile.setId(111L); profile.setEmployee(employee1); profileRepository.save(profile); Profile profile2 = new Profile(); profile2.setId(222L); profile2.setEmployee(employee2); profileRepository.save(profile2); Profile profile3 = new Profile(); profile3.setId(333L); profile3.setEmployee(hr); profileRepository.save(profile3); log.debug("Saved data to db"); }} Here, we created a CommandLineRunner bean that populates the database with sample data when the application starts.

Testing the ApplicationI have the following folder structure so far:

├── cerbos-policies│ ├── profile_test.yml│ ├── profile.yaml│ └── testdata│ ├── principals.yaml│ └── resources.yaml├── docker-compose.yml├── pom.xml├── src│ ├── main│ │ ├── java│ │ │ └── org│ │ │ └── cerbos│ │ │ └── demo│ │ │ ├── config│ │ │ │ └── CerbosConfig.java│ │ │ ├── controller│ │ │ │ └── ProfileController.java│ │ │ ├── Main.java│ │ │ ├── model│ │ │ │ ├── Employee.java│ │ │ │ └── Profile.java│ │ │ └── repository│ │ │ ├── EmployeeRepository.java│ │ │ └── ProfileRepository.java│ │ └── resources│ │ └── application.yml Of course, your Java package names can be different. Ensure you follow the structure for cerbos-policies.

First, you can test the Cerbos policies locally. Open a Terminal at the root of the project and run the Cerbos container:

docker-compose up

You should see a result like this:

my-cerbos-container | Test results my-cerbos-container | └──profileTestSuite (profile_test.yml) [32 OK] my-cerbos-container | my-cerbos-container | 32 tests executed [32 OK] my-cerbos-container exited with code 0 As you can see, the tests are successful. Later, we will run the tests automatically in our CI pipeline. We needed this step to make sure that the policies were correct. Comment out this line ( command: compile /policies) to keep the container running.

Run docker-compose up again.

Check that Cerbos is up and running by opening http://localhost:3592/ in your browser.

You should see a page like this:

Start the Spring Boot application from your IDE.

Since we enabled the H2 Console, we can see that the database contains the expected dummy data:

Let’s try to access the profile with ID 111 as an employee with ID 123:

$ curl -X GET http://localhost:8080/api/profile/get/111/123 Employee(employeeId=123, name=John Doe, email=john.doe@me.com, salary=1500.0, role=employee) The profile belongs to this employee, so it works as expected.

Now let’s try the same, but this time using the ID of the HR:

$ curl -X GET http://localhost:8080/api/profile/get/111/456 Employee(employeeId=123, name=John Doe, email=john.doe@me.com, salary=1500.0, role=employee) As expected, it shows the details because the HR has access to all actions.

Now, let’s try the DELETE request:

$ curl -X DELETE http://localhost:8080/api/profile/delete/111/123 Forbidden As anticipated, the employee is not allowed to delete their profile.

Let’s try with the hr role:

$ curl -X DELETE http://localhost:8080/api/profile/delete/111/456 Profile deleted successfully So far, everything works fine.

However, what will happen if an employee attempts to access someone else’s profile?

Let’s try to access the profile of employee1 with the employee ID of employee2:

$ curl -X GET http://localhost:8080/api/profile/get/111/321 Employee(employeeId=123, name=John Doe, email=john.doe@me.com, salary=1500.0, role=employee) Since we haven’t defined any custom rules, employees can currently access each other’s profiles. This is not the desired behavior, as we want to keep the information secure.

Cerbos addresses this issue using Conditions, which utilize Common Expression Language (CEL) syntax. You can add attributes to the principal and the resources to evaluate and enforce specific access conditions. For instance, you can check if a user’s address is within a certain geographic location, etc.

request: principal: id: john roles: - employee attr: geography: GB Checking the condition:

condition: match: all: of: - expr: > "GB" in R.attr.geographies - expr: P.attr.geography == "GB" Let’s refine our Cerbos policies by incorporating conditions.

Modify the resources.yaml to add a new attribute called owner:

resources: profile#1: id: profile#1 kind: profile attr: owner: 123 Add this line to the profile.yaml for the read action:

- actions: - read effect: EFFECT_ALLOW roles: ["*"] condition: match: expr: (request.resource.attr.owner == request.principal.id) || ('hr' in request.principal.roles) The policy allows any role to access the resource, provided they meet the condition specified in the match expression. This condition allows access if either the principal is the owner of the resource or holds the hr role.

We also need to adjust the Java code. Extend the code that gets the Principal and Resource in the getProfile() method with this:

Principal principal = Principal.newInstance(employeeId, employee.getRole()).withAttribute("id", AttributeValue.stringValue(employeeId)); Resource resource = Resource.newInstance("profile", profileId).withAttribute("owner",AttributeValue.stringValue(String.valueOf(profile.getEmployee().getEmployeeId()))); * Principal: Represents the user requesting access. The “id” attribute helps to identify and match the principal in access control decisions. * Resource: Represents the entity being accessed. The “owner” attribute specifies the resource’s owner and facilitates access control checks.

This setup allows Cerbos to enforce policies based on these attributes. For example, it ensures that only the resource owner or users with specific roles (like ‘hr’) can access or modify the resource.

The PDP receives these values and determines if the action is allowed based on the predefined policy.

The initial test cases will no longer work after these changes. We can extend the test suites by introducing multiple user resources and principals. Replace the content of the profile_test.yml with this:

name: profileTestSuitedescription: Tests for verifying the profile resource policyprincipals: hr: id: hr1 roles: - hr employee1: id: emp1 roles: - employee employee2: id: emp2 roles: - employee employee3: id: emp3 roles: - employeeresources: profile: kind: profile id: emp1 attr: owner: emp1 profile2: kind: profile id: emp2 attr: owner: emp2tests: - name: profile actions input: principals: - employee1 - employee2 - employee3 - hr resources: - profile - profile2 actions: - create - read - update - delete expected: - resource: profile principal: employee1 actions: create: EFFECT_DENY read: EFFECT_ALLOW update: EFFECT_DENY delete: EFFECT_DENY - resource: profile2 principal: employee2 actions: create: EFFECT_DENY read: EFFECT_ALLOW update: EFFECT_DENY delete: EFFECT_DENY - resource: profile principal: employee3 actions: create: EFFECT_DENY read: EFFECT_DENY update: EFFECT_DENY delete: EFFECT_DENY - resource: profile principal: hr actions: create: EFFECT_ALLOW read: EFFECT_ALLOW update: EFFECT_ALLOW delete: EFFECT_ALLOW - resource: profile2 principal: hr actions: create: EFFECT_ALLOW read: EFFECT_ALLOW update: EFFECT_ALLOW delete: EFFECT_ALLOW Automating Policy Tests with Semaphore CIAutomating policy tests in the CI/CD pipeline is a best practice. This means any misconfigured rules will cause the tests to fail so that you can react quickly.

In this section, you’ll learn how to integrate and run Cerbos policy tests within the Semaphore CI pipeline.

Semaphore requires a sempahore.yml file. This configuration file specifies the tasks and workflows that Semaphore CI should execute. It outlines the steps for building your application, running tests, and deploying changes. Create a new directory in the root of your project called .semaphore. Create the semaphore.yml file inside it:

version: v1.0name: Cerbos Policy Executionagent: machine: type: e1-standard-2 os_image: ubuntu2004blocks: - name: Compile Policies task: jobs: - name: Compile commands: - checkout - docker run -it --name my-cerbos-container -v ./cerbos-policies:/policies -p 3592:3592 ghcr.io/cerbos/cerbos:0.34.0 compile /policies - docker logs my-cerbos-container The semaphore.yml file performs the following tasks:

  • Sets up a virtual machine with ubuntu2004 image.
  • Runs the Docker container to compile the policies using the Cerbos command compile /policies.
  • Retrieves the container logs for review in the job’s output.

To run the tests in the pipeline, you need a free Semaphore account. Visit the signup page and choose either GitHub or Bitbucket for registration. In this tutorial, I will use GitHub.

Next, create a new project by clicking the “+ Create new” button.

I will connect my repository with Semaphore, which I used for this tutorial.

Semaphore will automatically initialize the project shortly. You can add more people to the project if you wish.

The final step is to create the workflow for the pipeline. With the semaphore.yml file already in place, you can use it directly for the workflow.

Let’s push something to the repository to trigger the pipeline.

For example, I added a README.md file to my project.

Shortly after the push, you’ll see that the pipeline will run the tests.

To see the details, click on the Compile job log.

As we added the docker logs command, we can access the container’s logs. There, you can see that all the tests were successful.

Let’s deliberately misconfigure the policies. For example, I’ll change the expected result for the hr role from EFFECT_ALLOW to EFFECT_DENY in one of the actions:

- resource: profile principal: hr actions: create: EFFECT_ALLOW read: EFFECT_DENY update: EFFECT_ALLOW delete: EFFECT_ALLOW Push the changes.

As expected, the job failed:

You can see in the logs which test failed:

ConclusionIn this tutorial, you learned what Cerbos is, and how to secure a Spring Boot application with Cerbos policies. You discovered how to create granular policies based on attributes to manage access control effectively.

Additionally, you set up automated policy testing with Semaphore CI. You can integrate this step into your existing pipeline.

By following this guide, you now have a solid foundation in leveraging Cerbos to enhance the security of your applications.

While this tutorial covered the basics of Cerbos, there are many more features to explore. For a deeper understanding, I encourage you to dive into the comprehensive Cerbos documentation.

You can find the source code of this tutorial in my GitHub repository.

Thank you for following along!

The post Securing a Spring Boot Application with Cerbos appeared first on Semaphore.

View Details

Marshalling client traffic to applications running in Kubernetes clusters, has for many years been the job of ingress controllers, using the Ingress API. However, the Ingress API has many limitations; it’s terse by design, ambiguous in places, and has no formal means for extending its limited capabilities. In short, without enhancement, it doesn’t meet the needs of the traffic management use cases in the real world. The Kubernetes community recognized the need for a better solution, and after several years in the making, this arrived in the form of the Gateway API.

The Kubernetes Gateway API reached GA status in October 2023 and continues to evolve as new features are identified and validated within the community. There are many improvements over the Ingress API:

  • Role-based – multiple object types (GatewayClass, Gateway, HTTPRoute, TCPRoute, and so on) replace the monolithic Ingress object type, to better reflect the fact that multiple actors need to control the configuration of the end-to-end ingress experience
  • Expansive – the Ingress API focuses on applications using the HTTP protocol, whereas the Gateway API caters for numerous protocols at different layers in the networking stack (HTTP/S, gRPC, TCP, UDP)
  • Expressive – the limited traffic management features in the Ingress API have been augmented to include traffic splitting and mirroring, request and response header modification, HTTP redirects and rewrites, and much more
  • Extendable – the unofficial, unregulated method of feature extension in the Ingress API, annotations, has been replaced with a more formal method of extension through ‘policy attachment‘

In this tutorial, you’ll use the Gateway API to configure ingress traffic to an example application running in a local Kubernetes cluster. You’ll be able to differentiate between the different Gateway API object types and see how they fit together to provide the full ingress experience for an application.

PrerequisitesThis tutorial uses a local development cluster using the Kind tool, where the cluster nodes are Docker containers. Docker is a prerequisite for using Kind. Follow the Quick Start guide to install Kind and to provision a local Kubernetes cluster.

To try and emulate operating in a cloud environment, the Cloud Provider for Kind should also be deployed. This will enable you to establish a local load balancer for use with the Gateway API.

Here’s a complete list of the prerequisites:

  • Docker (macOS, Windows, Linux – Docker Desktop or Linux – Docker Engine)
  • Kind
  • Cloud Provider for Kind
  • Kubernetes CLI – kubectl (macOS, Windows, Linux)

The Kubernetes configuration used in the tutorial can be found in the following GitHub repo:

  • https://github.com/nbrownuk/gateway-api-tutorial

Deploy an ApplicationWith a Kubernetes cluster up and running, it’s time to deploy a sample application to the cluster, which we eventually hope to consume from a web browser, once we’ve configured ingress using the Gateway API. The app simply displays a static web page providing some information about its environment.

Step 1: Apply the Kubernetes Configuration for the AppThe application can be deployed to the cluster using kubectl:

kubectl apply -f \ https://raw.githubusercontent.com/nbrownuk/gateway-api-tutorial/main/nginxhello.yaml Step 2: Check the App’s StatusAnd, we can check that it’s started up correctly, by retrieving the Pod, Service and Deployment objects that have the label app.kubernetes.io/name=nginxhello. We should see something like this:

$ kubectl -n nginxhello get po,svc,deployNAME READY STATUS RESTARTS AGEpod/nginxhello-cbfb6bbb6-zcqqq 1/1 Running 0 9sNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGEservice/nginxhello ClusterIP 10.96.243.155 <none> 80/TCP 9sNAME READY UP-TO-DATE AVAILABLE AGEdeployment.apps/nginxhello 1/1 1 1 9s We’ve got a Deployment with a single replica Pod, which is fronted by a Service of type ClusterIP.

With the app up and running, it’s time to turn our attention to the configuration of the Gateway API.

Deploy a Gateway ControllerJust like the older Ingress API in Kubernetes, there is no in-tree controller to act on instance objects of the Gateway API that are applied to the cluster. Instead, we have to deploy a third-party gateway controller of our choice. If you’re using an Amazon EKS cluster, it’s likely to be the AWS Gateway API Controller. And, if you’re using Microsoft’s AKS, it’s likely to be the Application Gateway for Containers ALB Controller. But, there are a whole bunch of different cloud-native proxies that implement the Gateway API, too, including the Envoy Gateway. The Envoy Gateway is based on the Envoy proxy, and it’s what we’ll use in this tutorial as the implementation of the Gateway API.

Step 1: Install the Envoy GatewayUnlike the Ingress API, which is a constituent part of the default set of Kubernetes APIs exposed by the API server, the Gateway API is provided as Custom Resource Definitions (CRDs). That means the CRDs need to be installed before they can be used. Sometimes, this installation is performed as part of the installation of the chosen gateway controller. Installing the Envoy Gateway gives us the CRDs, as well as the controller itself. There are a few ways to install the Envoy Gateway, but the project provides a handy install YAML file as part of its GitHub releases:

kubectl apply --server-side -f \ https://github.com/envoyproxy/gateway/releases/latest/download/install.yaml It will take a few moments before the Envoy Gateway is ready in the cluster.

Step 2: Check the Status of the Envoy GatewayThe installation should give us the Envoy Gateway deployed to a new namespace in our cluster, envoy-gateway-system, which we can check by retrieving the Pod, Service and Deployment objects in that namespace:

$ kubectl -n envoy-gateway-system get po,svc,deployNAME READY STATUS RESTARTS AGEpod/envoy-gateway-654d5d67f8-pzmr6 1/1 Running 0 34sNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGEservice/envoy-gateway ClusterIP 10.96.79.24 <none> 18000/TCP,18001/TCP,18002/TCP,19001/TCP 34sNAME READY UP-TO-DATE AVAILABLE AGEdeployment.apps/envoy-gateway 1/1 1 1 34s If the Pod status is Running, all should be well with the gateway controller.

Step 3: Create a GatewayClassBut, it’s possible to run more than one gateway controller in a cluster; different controllers for different purposes, or horses for courses. So, we must associate the gateway controller with a GatewayClass object that can be referenced by other Gateway API objects we want to create, so that the correct controller is selected for the job. The GatewayClass definition that we need for the Envoy Gateway looks like this:

```

gateway-class.yaml---apiVersion: gateway.networking.k8s.io/v1kind: GatewayClassmetadata: name: envoy-gatewayspec: controllerName: gateway.envoyproxy.io/gatewayclass-controller

`` The value of thecontrollerNamefield is the same as the default name associated with the Envoy Gateway controller. We can see this by inspecting theConfigMap` that is used to configure the Envoy Gateway during installation:

$ kubectl -n envoy-gateway-system get cm envoy-gateway-config \ -o "jsonpath={.data['envoy-gateway\.yaml']}"apiVersion: gateway.envoyproxy.io/v1alpha1kind: EnvoyGatewaygateway: controllerName: gateway.envoyproxy.io/gatewayclass-controllerlogging: level: default: infoprovider: type: Kubernetes Note the value of the controllerName in the EnvoyGateway definition. So, the GatewayClass object definition we’re about to use has a correlation with the Envoy Gateway by virtue of the controllerName. But, it needs to be applied to the cluster to take effect:

kubectl apply -f \ https://raw.githubusercontent.com/nbrownuk/gateway-api-tutorial/main/gateway-class.yaml Step 4: Examine the GatewayClass ObjectOnce the cluster-scoped object has been created in the cluster, we can check its status:

$ kubectl get gc envoy-gatewayNAME CONTROLLER ACCEPTED AGEenvoy-gateway gateway.envoyproxy.io/gatewayclass-controller True 7s It should have an ‘Accepted’ status of ‘True’ to indicate the Envoy Gateway has accepted processing on behalf of the GatewayClass. We’re now set up to make use of the Envoy Gateway to handle ingress traffic to the cluster.

Configure a GatewayThe next part of the process is to configure another Gateway API component for our scenario; a Gateway object.

Step 1: Define a GatewayA Gateway defines a set of ‘logical endpoints’ that are associated with an IP address. You can think of a Gateway as defining the characteristics of the traffic that can enter the cluster at the edge. Gateway controllers act on Gateway definitions to allow the ingress of traffic to the cluster. Depending on its type, the gateway controller might provision cloud infrastructure resources, or configure a proxy on behalf of the Gateway. The endpoints can be addressed by clients internal or external to the cluster, that wish to consume the apps running in the cluster. The logical endpoints are called ‘Listeners’, and are constituent parts of Gatewaydefinitions:

```

gateway.yaml---apiVersion: gateway.networking.k8s.io/v1kind: Gatewaymetadata: name: http-gw namespace: nginxhellospec: gatewayClassName: envoy-gateway listeners: - name: http protocol: HTTP port: 80

`` Listeners can define different network protocols; HTTP, HTTPS, TCP and UDP. Here, we’ve got a Listener specifying the HTTP protocol for traffic destined for theGatewayon port 80. Notice also that theGatewayreferences the Envoy Gateway instance with the associatedGatewayClassnamedenvoy-gateway`. It’s this gateway controller that acts on its behalf.

Step 2: Create a GatewayThe Gateway definition needs to be applied to the cluster:

kubectl apply -f \ https://raw.githubusercontent.com/nbrownuk/gateway-api-tutorial/main/gateway.yaml Once the API server has created the object, the Envoy Gateway acts on the definition to enable the ingress of HTTP traffic on port 80. But, what actions does the Envoy Gateway take? We can check in on the envoy-gateway-system namespace to see:

$ kubectl -n envoy-gateway-system get po,svc,deploy \ -l gateway.envoyproxy.io/owning-gateway-name=http-gwNAME READY STATUS RESTARTS AGEpod/envoy-nginxhello-http-gw-ca0a3c04-55ddfb67fc-6njpv 2/2 Running 0 16sNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGEservice/envoy-nginxhello-http-gw-ca0a3c04 LoadBalancer 10.96.21.92 172.18.0.3 80:32597/TCP 16sNAME READY UP-TO-DATE AVAILABLE AGEdeployment.apps/envoy-nginxhello-http-gw-ca0a3c04 1/1 1 1 16s There are some additions to the namespace that have been brought about by the Envoy Gateway. Now there is a replicated deployment of the Envoy proxy, exposed via a LoadBalancer type Service, with an externally addressable IP address on port 80. The IP address, provisioned by the Cloud Provider for Kind, is 172.18.0.3. Be sure to note which EXTERNAL-IP your env has given you – you’ll need it in the next steps! Clients can send HTTP requests to this IP address when they need to communicate with an app running in the cluster. The set up looks something like this:

Step 3: Examine the Gateway ObjectWe can also check the status of the created Gateway object:

$ kubectl -n nginxhello get gtw http-gwNAME CLASS ADDRESS PROGRAMMED AGEhttp-gw envoy-gateway 172.18.0.3 True 42s This tells us the IP address associated with the Gateway (it’s the same as the externally addressable LoadBalancer Service), and that it has been programmed into the data plane by the Envoy Gateway.

But, which app do client requests get sent to? We haven’t yet defined a route to a backend service running in the cluster.

Define RoutesThe final piece of the jigsaw is to define an HTTPRoute object that will allow traffic to be sent to the application that we deployed right at the beginning of this tutorial.

Step 1: Define an HTTPRouteThe HTTPRoute will look like the following:

```

httproute.yaml---apiVersion: gateway.networking.k8s.io/v1kind: HTTPRoutemetadata: name: nginxhello namespace: nginxhellospec: parentRefs: - name: http-gw hostnames: - 172-18-0-3.nip.io # to be replaced with your EXTERNAL-IP rules: - backendRefs: - name: nginxhello port: 80

`` Firstly, there is a reference to theGatewaythat will be responsible for this route, the one we’ve just established in the cluster, calledhttp-gw. TheHTTPRoutewill be ‘attached’ to the HTTP Listener in theGateway. Then, we define a list of hostnames that must match an incoming HTTP request host header. There is a single entry;172-18-0-3.nip.io1. We’re using the nip.io service to give us a domain name for the IP address of the LoadBalancer service associated with the Gateway in our local environment. Finally, we have a single rule, which applies no conditions, and simply references theClusterIP` Service that fronts our application. Any client HTTP requests addressed to the domain name will get routed to the application as a result.

Step 2: Create an HTTPRouteThe HTTPRoute needs to be applied to the cluster.

kubectl apply -f \ https://raw.githubusercontent.com/nbrownuk/gateway-api-tutorial/main/httproute.yaml Step 3: Examine the HTTPRoute ObjectThe first check we should make is to ensure the HTTPRoute object was created successfully:

$ kubectl -n nginxhello get httproute nginxhelloNAME HOSTNAMES AGEnginxhello ["172-18-0-3.nip.io"] 11s This response doesn’t tell us too much, other than the object has been created successfully. What is of more importance to us, is whether the HTTPRoute has been accepted by the Gateway, and we need to get more information by describing the object:

kubectl -n nginxhello describe httproute nginxhello We’ll get a lot of information back, but if we look out for the ‘status’ information provided, we should see something like this:

<snip>Status: Parents: Conditions: Last Transition Time: 2024-05-29T13:41:39Z Message: Route is accepted Observed Generation: 1 Reason: Accepted Status: True Type: Accepted<snip> The status tells us that the HTTPRoute has been accepted by the Gateway, and is now attached to it for processing purposes. Our scenario is now complete:

We’re good to go!

Step 4: Consume the ApplicationThe final step is to use a web browser to attempt to consume the application, by using the hostname specified in the HTTPRouteas the URL. It’s a non-TLS connection, so do accept the security exception, and the app should respond to the request accordingly:

ConclusionThis tutorial provides a basic introduction to the capabilities of the Gateway API. It has much more to offer and represents a big improvement on the frailties of the Ingress API for ingress scenarios. It still has a way to go before it reaches full maturity, but it’s already mature enough to be used in production clusters. If you’re considering moving your existing Ingress definitions over to the Gateway API, check out the Ingress2Gateway utility, which provides translation for a growing set of ingress providers.

If the IP address of your Gateway is different, be sure to amend the hostname accordingly. ↩

Learn more:

  • Blue-Green Deployments With Kubernetes
  • Top 10 Must-Have Tools for Kubernetes Engineers
  • Guide to the Rolling Update Deployment Strategy

The post How to Expose Kubernetes Apps Using the Gateway API appeared first on Semaphore.

View Details

Search functionality is key to the success and usability of any modern application, as users want to find relevant information quickly and efficiently. Over time, Elasticsearch has become the go-to search solution for many organizations. However, despite its popularity and broad feature set, it may not be the best option for startups due to its complexity, cost, and steep learning curve. No wonder startups are interested in more agile Elasticsearch alternatives.

In this article, you will explore why startups should look into Elasticsearch alternatives for their search solution. Then, you will see the key aspects to consider when comparing search solutions and apply them to the top 7 alternative search services for startups.

Let’s dive in!

Why Elasticsearch May Not Be the Best Search Solution for a StartupElasticsearch is the most popular distributed, RESTful search and analytics engine. It falls into the category of search solutions, which provide powerful full-text and vector search capabilities over large datasets, along with features for logs, metrics, and APM (Application Performance Monitoring).

The end goal is to enable users to quickly find relevant information, even with typos in their search queries. Admins can then analyze search data to find patterns, make decisions, and improve business processes.

Integrating an Elasticsearch-powered search feature in your application results in improved user experience and efficiency. Despite these benefits, Elasticsearch may not be the best choice for startups for three key reasons:

  • High costs: Handling an Elasticsearch architecture on platforms like AWS can become increasingly costly as data volumes grow.
  • Too many features: While beneficial for large enterprises, the extensive feature set of Elasticsearch can be overwhelming and unnecessary for smaller startups.
  • Difficult setup and steep learning curve: Setting up Elasticsearch and getting the best out of it requires significant expertise and time.

Startups generally need affordable, simple, and flexible solutions that are easier to set up and maintain. The technology stack should evolve quickly alongside the core business, without the burden of complex infrastructure management. This is why they love to explore some Elasticsearch alternatives.

Aspects to Consider When Evaluating Search ServicesNow, assume you are a startup looking for a good Elasticsearch alternative. These are the main elements to keep in mind when comparing search solutions:

  • Type: Is it open-source, proprietary, self-hosted, or a cloud service?
  • Search types: What are the underlying algorithms? (e.g., vector search, geo search, nearest neighbor search, etc.)
  • Features: The list of the main features provided by the search service.
  • Pros and cons: The advantages and drawbacks of the solution.
  • API clients: The available API client libraries (and programming languages) to interact with the search engine and simplify data retrieval.
  • Popularity: Useful statistics about the solution, such as the review score, number of GitHub stars, downloads, installations, users, and more.
  • Pricing: The pricing model of the solution, in case of a premium service.

Top 7 Elasticsearch Alternatives for StartupsLet’s use the criteria presented above to list and compare the best Elasticsearch alternatives for startups.

If you are eager to find out what these solutions are and what they offer, take a look at the table below:

| Feature | Algolia | Typesense | ZincSearch | OpenObserve | OpenSearch | Vespa | | --- | --- | --- | --- | --- | --- | --- | | GitHub stars | — | 18.6k | 16.7k | 10.2k | 9k | 5.4k | | First public commit | 2012 | 2015 | 2021 | 2023 | 2014 | 2016 | | Programming language | C++ | C++ | Go | Rust | Java | Java, C++ | | Source code | Proprietary | Open source | Open source | Open source | Open source | Open source | | Deployment | Cloud | Self-hosted/Cloud | Self-hosted | Self-hosted/Cloud | Self-hosted/Cloud | Self-hosted/Cloud | | Pricing | Premium | Free/Premium | Free | Free/Premium | Free | Free/Premium | | Search types | Hybrid, semantic, and more | Vector, semantic, and more | Full-text | Log | Fuzzy, wildcard, and more | Fuzzy, vector, and more | | API clients | PHP, JS, Ruby, Python, iOS, Android, C#, Java, Go, Scala, Rails, Symfony, Django, Laravel, Kotlin, Elixir, ColdFusion, Erlang, .NET | JavaScript, PHP, Python, Ruby, Go, .NET, Java, Rust, Dart, Perl, Swift, Clojure | Go, Python, JavaScript | — | Python, Java, JavaScript, Go, Ruby, PHP, .NET, Rust | Python, Java |

Time to explore the seven best startup-oriented search solutions!

Algolia is a cloud-based, hosted search engine that provides real-time search results through a comprehensive API interface. It consists of two main components:

  1. Search Implementation: Enables developers to create and maintain excellent search experiences for users.
  2. Search Analytics: Empowers business teams to analyze and optimize these experiences to meet evolving business objectives.

Algolia is an end-to-end AI search and discovery platform that offers fast and reliable search capabilities in more than 68 languages. Its cloud-native nature makes it more agile and easier to roll out than Elasticsearch. This is especially important for startups, which are always looking for quick setups and integrations.

Explore the official site to find out why Algolia is a good Elasticsearch alternative.

📚 Type: End-to-end AI search and discovery platform

🔎 Search types: Hybrid keyword and vector search, semantic search, faceted search, and geo search via NeuralSearch

🛠️ Features:

  • Support for over 68 languages
  • Indexing, auto complete, and stemming
  • Configurable typo tolerance
  • Highlighted results to show which section and words matched user queries
  • Advanced language processing, like options for plurals handling, stop words, optional words, and prefixing behavior
  • Synonym handling
  • Support for location-based queries with geo-aware search results
  • Multiple sorting strategies
  • Grouping and deduplication of records
  • Advanced personalization capabilities so that different users get different results for the same queries
  • Search analytics available in a visual dashboard
  • AI recommendations

👍 Pros:

  • Average response time of 1 to 20 milliseconds for query processing
  • Over 100 releases per year
  • 99.999% uptime guarantee via SLA
  • Dozens of community integrations with CMS and e-commerce platforms

👎 Cons:

  • Proprietary search engine with vendor lock-in
  • Can become expensive, especially on projects with high search volumes and large datasets

🔗 API clients:

  • Official API clients for PHP, JavaScript, Ruby, Python, iOS, Android, C#, Java, Go, Scala, Rails, Symfony, Django, Laravel, Kotlin
  • Community API clients for Elixir, ColdFusion, Erlang, .NET

📊 Popularity:

  • 17k paying customers
  • 1.7+ trillion searches every year
  • 500k+ developers using Algolia
  • 30 billion indexed records
  • 4.4/5 on Gartner Peer Review
  • 4.5/5 on G2
  • 4.7/5 on Capterra

💰 Pricing:

  • Build:
    • Free plan
    • Up to 10k search requests/month, with 1m records included
  • Grow:
    • Free/pay-as-you-go plan
    • Free up to 10k search requests/month, then $0.50 per additional 1k search requests
    • Free up to 100k records, then $0.40 per additional 1k records/month
  • Premium:
    • Search experience with some AI features
    • Contact sales for pricing
  • Elevate:
    • Full AI search offering
    • Contact sales for pricing

TypesenseTypesense is a typo-tolerant search engine optimized for instant search-as-you-type experiences. It serves as an open source alternative to Algolia—addressing some of its key issues—and represents a batteries-included alternative to ElasticSearch.

The team designed every feature from the ground up, with a close eye on making it intuitive and easy to use. This makes Typesense ideal for startups seeking an easy-to-integrate, quick-to-setup, and fast search solution.

Discover how Typesense compares to alternatives.

📚 Type: Open source, typo-tolerant search engine

🔎 Search types: Vector search, semantic search, geo search, fuzzy search, conversational search, image and voice search

🛠️ Features:

  • Typo tolerance
  • Meticulously architected from the ground up for low-latency instant searches.
  • Support for dynamic sort results based on a particular field at query time
  • Advanced faceting and filtering capabilities
  • Support for grouping results and getting distinct records
  • Support for federated search across multiple collections in a single HTTP request
  • Ability to search and sort by results around a latitude/longitude or within a bounding box
  • Ability to send questions and receive responses with fully-formed sentences, as with ChatGPT, but over your own data.
  • Possibility to search through images using text descriptions
  • Capture and send queries via voice recordings
  • Connect one or more collections via common reference fields and join them during query time.
  • Synonym handling
  • Curation and merchandizing to boost particular records to a fixed position in the search results

👍 Pros:

  • Batteries-included developer experience with a 30-second setup
  • Several examples with code covering different real-world use cases
  • Integration with Firebase, AWS DynamoDB, MongoDB, Supabase, Laravel, WordPress, Magento, and popular documentation technologies
  • In-depth guides to simplify migration from Algolia

👎 Cons:

  • No out-of-the-box user-level search personalization
  • Implementing search recommendations requires custom implementation

🔗 API clients:

  • Official API clients in JavaScript, PHP, Python, Ruby
  • Community API clients in Go, .NET, Java, Rust, Dart, Perl, Swift, Clojure

📊 Popularity:

  • 18.6k stars on GitHub (41 committers)
  • 14 million docker pulls
  • 5 billion cloud searches per month
  • 5/5 on G2

💰 Pricing:

  • Open Source (self-hosted): Free via a GPL license
  • Typesense Cloud: Prices change based on memory and CPU resources

ZincSearchZincSearch is an open source and lightweight alternative to Elasticsearch. It provides a search engine written in Go that requires low resources and is designed for full-text indexing. Under the hood, it relies on bluge as the underlying indexing library.

ZincSearch represents a free drop-in replacement for Elasticsearch if you are searching using Kibana and ingesting data using Ingest APIs. Note that Kibana is not supported with ZincSearch, as this provides its own basic UI for data exploration.

📚 Type: Open source search engine for full-text indexing

🔎 Search types: App search

🛠️ Features:

  • Full-text indexing capabilities
  • Web UI for querying data written in Vue.js
  • Compatible with Elasticsearch APIs for data ingestion
  • Out of the box authentication system
  • Schemaless data representation
  • Index storage in disk
  • Support for data aggregation

👍 Pros:

  • Easy installation through binary files for multiple platforms
  • Docker image for quick setup
  • Complete configuration in about 2 minutes
  • Simple, developer-oriented, lightweight tool

👎 Cons:

  • Not designed for log search
  • No high availability architecture

🔗 API clients:

  • Official API clients in Go, Python, JavaScript

📊 Popularity:

  • 16.7k stars on GitHub (57 committers)

💰 Pricing: Free

OpenObserveOpenObserve is an open source, cloud-native log search, infrastructure monitoring, and APM solution. It is designed to handle logs, metrics, traces, and analytics, offering full compatibility with the OpenTelemetry standard.

You can think of OpenObserve as a mix of Prometheus for metrics, Elasticsearch for logs, Jaeger for traces, and Grafana for dashboards. Thanks to its compatibility with several storage services, it can significantly reduce storage costs—promising to lower them to about 140 times less than Elasticsearch.

Check out the docs to see why OpenObserve is a good alternative to Elasticsearch.

📚 Type: Open source and cloud-native observability platform built specifically for logs, metrics, traces, and analytics

🔎 Search types: Log search

🛠️ Features:

  • Support for storing and exploitation of logs, metrics, and traces
  • Full compatibility with the OpenTelemetry standard
  • Real User Monitoring (RUM) via performance tracking, error logging, and session replay
  • Over 14 different chart types for comprehensive data visualization
  • Advanced ingest and query functions for data redaction, log reduction, and compliance
  • Support for querying logs and traces via SQL and metrics via SQL and PromQL
  • Advanced embedded GUI with an intuitive interface
  • Versatile storage options, including local disk, S3, MinIO, GCS, Azure Blob Storage
  • High availability and clustering to ensure reliable and scalable performance
  • Dynamic schema to adapt your data structure seamlessly
  • Built-in authentication with RBAC privilege system
  • UI available in 11 languages, including English, Spanish, German, French, Chinese, and more

👍 Pros:

  • Open source
  • Docker image available for easy setup
  • First configuration in less than 2 minutes
  • Integration with Slack, Amazon S3, Microsoft Teams, Google Cloud Storage, and Minio

👎 Cons:

  • Not so easy to integrate with other technologies
  • Limited to log search

🔗 API clients: None

📊 Popularity:

  • 10.2k stars on GitHub (53 committers)

💰 Pricing:

  • Open Source (self-hosted): Free via an AGPL license
  • Open Source (self-hosted): Paid via an enterprise license
  • OpenObserve Cloud: 4 different plans, from a free plan for developers to a plan dedicated to enterprises

OpenSearch

OpenSearch is an open source software suite for search, analysis, and observability. Forked from Elasticsearch 7.10, it maintains backward compatibility with Elasticsearch’s REST API, query syntax, and responses. That makes it the most straightforward drop-in solution among the Elasticsearch alternatives in this list.

OpenSearch allows users to seamlessly ingest, protect, search, aggregate, visualize, and analyze data, just as in Elasticsearch, but with all the benefits of open source. Its community nature and origins make it ideal for startups, as you will find many users to learn and share lessons learned with.

Read more about why OpenSearch is not Elasticsearch.

📚 Type: Open source search and analytics suite

🔎 Search types: Fuzzy search, wildcard search, geo search, vector search, neural search

🛠️ Features:

  • Application and infrastructure monitoring with log data storage and automated alerts
  • Centralized logs for real-time security monitoring and forensic analysis
  • Observability capabilities through logs, metrics, and traces for real-time application and business monitoring
  • Anomaly detection to identify and notify about atypical data
  • KNN (K-nearest neighbors) for finding nearest neighbors in vector data
  • Performance analyzer to monitor and optimize cluster performance
  • SQL and piped processing language support for querying data
  • Advanced indexing capabilities
  • Asynchronous search for running background search requests
  • Cross-cluster replication for high availability and disaster recovery
  • Extensible architecture with dozens of plugins available

👍 Pros:

  • Community-oriented project with a lot of active users on GitHub and Slack
  • Easy setup via Docker
  • Highly fast and scalable
  • OpenSearchCon to meet the community and see the search suite in action

👎 Cons:

  • Can be complex to be configured for optimal performance
  • Same learning curve as Elasticsearch

🔗 API clients:

  • Official API clients in Python, Java, JavaScript, Go, Ruby, PHP, .NET, Rust, Rust, Hadoop

📊 Popularity:

  • 9k stars on GitHub (302 committers)
  • 4.2/5 on G2

💰 Pricing: Free

Vespa

Vespa is an open source search engine and vector database. Originally launched as a pioneering search engine in 1997, it became part of Yahoo through the Overture deal in 2003 and was open sourced in 2017.

Vespa supports vector search, lexical search, and search in structured data, all within the same query. Its integrated machine-learned model inference enables real-time application of AI, making it great for startups needing to quickly derive insights from data.

Delve into the relationship between Vespa, Elasticsearch, and Solr.

📚 Type: Open source search engine and vector database

🔎 Search types: Fuzzy search, vector search, geo search, lexical search, streaming search

🛠️ Features:

  • Ability to add, modify, and remove documents with durable write acknowledgments in milliseconds
  • Automatic data corruption repair from uncorrupted replicas
  • Simple HTTP/2 API for writing and retrieving data
  • Response times typically in tens of milliseconds
  • Rich data schemas that allow various primitive types, collections, structs, and tensors
  • Referencing and querying fields from referenced documents can be done without performance penalties
  • Queries support with structured filters, free text, and vector search operators
  • Streaming search mode for low-latency access to billions of documents per node
  • Result ranking via configurable functions, including machine-learned models and ONNX integration
  • Indexing with linguistic transformation
  • Support for multiple ranking phases and advanced text ranking features
  • Support for nested groupings and data aggregation
  • Support for multiple clusters of any size with fault tolerance and automatic node routing
  • Tokenization and stemming in English, German, French, Spanish, and Italian

👍 Pros:

  • Easy installation via RPM files, Docker image, or deployment in AWS
  • Application configuration specified in a single application package for simplified deployment
  • Multi-tenant architecture with no single points of failure
  • Real-time data ingestion, processing, and streaming

👎 Cons:

  • Confusing documentation
  • Only a few languages fully supported, limiting the company’s growth in international markets

🔗 API clients:

  • Official API clients in Python and Java

📊 Popularity:

  • 5.4k stars on GitHub (79 committers)

💰 Pricing:

  • Open Source (self-hosted): Free via an Apache 2.0 license
  • Vespa Cloud: 3 different plans, from a basic plan for individuals to a plan dedicated to enterprises

Solr

Solr is an enterprise search platform built on Apache Lucene, an open source, Java-based, information retrieval library. It supports advanced full-text search with advanced filtering capabilities. Solr is suitable for startups that need to handle scalable volumes of traffic.

It is based on open standards and is designed to drive powerful document retrieval applications. In particular, it responds with structured documents to simple HTTP requests.

📚 Type: Open source enterprise search platform built on Apache Lucene

🔎 Search types: Wildcard search, spatial search, dense vector search, fuzzy search, proximity search, range search

🛠️ Features:

  • Documents can be added via JSON, XML, CSV, or binary over HTTP
  • Querying via HTTP with results in JSON, XML, CSV, or binary
  • Advanced full-text search capabilities
  • Comprehensive built-in admin UI
  • Support for near real-time indexing for quick updates
  • Extensible architecture that supports plugins for custom functionalities
  • High schema flexibility with both schemaless and configured schema modes
  • Powerful faceted search and filtering capabilities.
  • Built-in support for geospatial search for location-based queries
  • Advanced configurable text analysis in several languages

👍 Pros:

  • Standalone search server with a REST-like API interface
  • Based on open interfaces like XML, JSON, and HTTP
  • Ideal for handling high-volume traffic at large scales
  • High performance and scalability

👎 Cons:

  • Hard-to-explore documentation that can be overwhelming for new users
  • Confusing query language

🔗 API clients:

  • Official API client library in Java
  • 3rd-party API clients in Python, Ruby, PHP, C#/.NET, Scala, Perl, JavaScript, Clojure, Go, Rust, R, C++

📊 Popularity:

  • 1.1k stars on GitHub (387 committers)
  • 4.1/5 on G2
  • 4.3/5 on Capterra

💰 Pricing: Free

ConclusionIn this guide, you learned why Elasticsearch may not be the best search solution for a startup. Fortunately, there are more affordable, open source, and easier-to-configure technologies for searching data. Here, you explored the seven best Elasticsearch alternatives with quick setups and low resource usage. As a startup, knowing these affordable solutions can help you save a lot of time and money!

The post 7 Elasticsearch Alternative Search Solutions for Your Startup appeared first on Semaphore.

View Details

Flaky tests—those intermittent failures that undermine confidence in test suites—are a persistent thorn in the side of development teams. As software complexity grows, so does the challenge of ensuring test reliability. In this episode, Srivishnu Ayyagari, a senior product manager at LambdaTest, offers valuable insights into the root causes of flaky tests and strategies to overcome them.

Edited transcriptionThe type of application and use case defines the testing strategy, says Srivishnu Ayyagari, senior product manager at cloud-based testing platform LambdaTest. For example, in e-commerce applications, “elements or components stay in the same place”, and consequently, require developers to “run the same test case every day.” In these cases, automated testing has become vital, allowing testers to automate the execution of tests on “different browsers and validate their use cases” through test scripts.

Aware of the potential of test automation, the tooling market continues developing software that further increases the reach of what’s possible to automate. In this way, since Srivishnu arrived at LambdaTest, he has been part of their transition from developing manual to automated testing tools. As he explains, working with “different kinds of open source frameworks like Selenium, Cypress, and Playwright” has allowed LambdaTest to “create a plethora” of valuable tools for QA professionals, including “Hyper Execute, Insight and SmartUI for visual regression testing and real-device mobile app testing.”

Understanding —and addressing— test instabilityFlaky tests (tests that intermittently pass or fail without any code changes) are the Achilles heel of test automation. The time burden of determining if a test failure indicates a bug or a false positive collides with the swiftness of automation. Furthermore, flaky tests seem unavoidable in today’s development, due to their tight correlation with current practices and architectural choices.

The rise of flaky tests, Srivishnu explains, goes hand in hand with the popularity of microservices architecture, in which “there’ll be different kinds of service, but there’ll be one single application, client application, which will be talking to different kinds of APIs.” Each of these APIs “might not perform at the same level compared to others;” moreover, different services might have varying reliance on external APIs or systems, which can introduce unpredictability. Besides, asynchronous operations or race conditions can lead to inconsistent results, as well as differences in testing environments (local, CI/CD, cloud).

Another root cause behind flakiness is a combination of asynchronous loading time of page elements and test script execution speed. The time it takes for elements to appear on a webpage can vary due to network conditions, device performance, and other factors —for example, the use of third-party features that have to be retrieved from external websites. Test scripts, in turn, often execute faster than page loading, leading to issues when trying to interact with elements that aren’t fully loaded.

In this case, mitigation strategies involve adding deliberate delays in test scripts to ensure elements are loaded before interacting with them and configuring the browser to wait for a specific amount of time before timing out. Likewise, it also helps to improve website performance and reduce the time it takes for elements to load.

Network latency, or the time it takes for data to travel between different locations, is another significant contributor to flaky tests. When applications and their components are distributed across multiple data centers, network performance can dramatically impact test reliability. “For example, a test might pass when running from a location close to the application’s server but fail when executed from a remote location due to increased network latency,” Srivishnu explains. To mitigate network latency, he recommends isolating tests and ensuring adequate network bandwidth. Besides, retrying failed tests will help differentiate between genuine application issues and network-related flakiness.

In all situations, when a “flaky test is found,” says Srivishnu, “you have to take a retry” to “check whether it is a correct result or is in the false positive result.” Here, repetitive failing after consecutive runs means “it is a hundred percent feature problem” that the developers need to address and fix. In turn, if it fails randomly after several runs, “that means that there’s an issue with the execution of the browser test,” and the solution involves checking “whether that command is being called correctly every time” and adding mitigation steps. In this regard, many testing frameworks now include built-in retries to handle transient failures.

Addressing flaky tests with LambdaTestSrivishnu encourages proactively addressing flaky tests in a test suite such as LambdaTest. Early detection and mitigation are key to maintaining healthy development cycles. “Once your automation tests start growing, it will be very hard to manage and see how many of the tests are giving false positive results,” he points out.

Beyond simply identifying flaky tests, understanding the underlying causes is part of remediation. To this end, LambdaTest features error categorization insights. “Providing users with just information about flaky tests wasn’t enough,” explains Srivishnu. “We wanted to give them deeper insights into the specific errors causing the flakiness.” Lambda Test’s error categorization feature breaks down flaky tests by the type of error encountered, such as “no such element” or “stale element reference” so developers can learn which are the most prevalent issues and prioritize their efforts.

Additionally, LambdaTest uses AI to identify patterns in test failures and flag potential flaky tests. To find these patterns, the platform tries combinations of different variables, such as system OS, browsers, and resolutions. At this point, LambdaTest provides data on the frequency and severity of flaky tests on these configurations and if they exceed the flaky threshold. LambdaTest also uses HyperExecute to isolate tests and limit the interference of environmental factors and test dependencies.

The effectiveness of flaky test detection can vary depending on the testing framework used. “The approach to flaky test detection is multifaceted,” says Srivishnu. “Different frameworks have their strengths, and it’s essential to choose the right tools and strategies based on project requirements and team preferences.” As such, while there are common strategies, specific implementations differ across tools. Playwright, for example, employs a retry mechanism to handle potential flakiness. If a test fails, it’s automatically retried a specified number of times. If the test passes on subsequent attempts, it’s flagged as potentially flaky. For Selenium-based testing, tools like Report Portal can be integrated to analyze test logs and identify patterns indicating flakiness. Cypress, another popular framework, offers its cloud-based solution for detecting flaky tests and providing insights into the specific commands causing issues.

Emerging testing frameworks and trendsBeyond automated testing, the broader context of UI technologies has implications for QA strategies. While React remains dominant, other frameworks have gained traction, each offering unique advantages. “We have seen adoption starting recently with the launch of Next.js, where most of the front-facing pages, so let’s say customer-facing pages, are mostly driven with Next.js.” Vue.js, for its part, provides a distinct architectural approach that resonates with many developers.

Concurrently, the testing framework ecosystem is also transforming. Playwright has emerged as a strong contender, distinguishing itself with its WebSocket-based communication, which significantly reduces latency compared to traditional HTTP-based approaches. As Srivishnu explains, “Playwright has seen a huge massive adoption being increasing and competing directly with Cypress as well as Selenium, where the major, game-changing thing that they have launched was the WebSocket connection.” Selenium, a long-standing industry stalwart, is responding to these developments by exploring a more interactive, by-directive approach to keep pace with modern testing requirements.

A key question regarding Playwright’s WebSocket-based approach is browser compatibility. Srivishnu clarifies that WebSocket support is indeed provided by the browsers themselves. “These are exposed by the browsers themselves to have a WebSocket-based interaction,” he explains.

Srivishnu strongly advocates for Playwright as the best starting point for teams looking to automate their test suites across various frameworks. “Playwright is JavaScript natively and JavaScript is very easy to code and get to know like prod as well,” he emphasizes.

Beyond its accessibility, Playwright offers a rich set of features for browser manipulation and testing. Its advanced command set and active open-source community further solidify its position as a preferred choice. “The best thing with Playwright is that all the commands or let’s say the APIs are much more advanced and they can do a lot of browser manipulation, DOM manipulation, like capture a screenshot, do a visualization test…” Srivishnu explains.

Another key advantage of Playwright is its speed. Through WebSocket connections, Playwright can significantly reduce test execution time compared to Selenium. “Playwright runs faster than Selenium because of that specific WebSocket feature,” Srivishnu affirms. Faster test execution translates to quicker feedback loops and increased developer productivity.

The bottom lineVisit Lambdatest.com to learn more about the platform and its resources, including a learning hub for testing, webinars, and test automation certifications. Connect with Srivishnu on Linkedin.

The post Srivishnu Ayyagari On Flaky Tests Anti-Patterns appeared first on Semaphore.

View Details

Traditional database hosting services, like cloud services, offer excellent performance but come with significant costs. However, not all projects require many dedicated resources. Considering that developers are constantly seeking affordable solutions, the database hosting landscape has evolved significantly in recent years. Thanks to the emergence of new players, numerous low-cost alternatives are now available for individuals and businesses considering database migration.

In this article, you will dig into popular database services to understand what they offer and how much they cost. Then, you will explore five cost-effective alternatives that guarantee affordability without compromising on functionality.

Let’s dive in!

The Real Cost of Database HostingIn most cases, databases themselves do not cost money since they are open-source technologies. This means you do not have to pay for the database software itself. Instead, the primary cost comes from hosting.

Database hosting services provide essential infrastructure, security, and maintenance to keep your database operational. These usually charge based on how large the servers used to host your databases are.

Additionally, there may be fees for database migration, when transferring data from one server to another. That is common with cloud providers. These fees, also known as egress fees, are typical in cloud storage systems like S3 but might also apply to databases. For more information, read our guide on how to avoid egress fees with Cloudflare R2.

Understanding the real costs behind databases is crucial for budgeting effectively and selecting cost-efficient solutions that fit your needs.

Database Hosting Services: Types, Cost, and ProvidersDatabase hosting is the process of storing databases on remote servers provided by a third-party company. That enables individuals and companies to access and manage their databases without needing to own and operate the physical hardware themselves.

Typically, database hosting is a service offered by a specialized provider. This service provides everything required to set up and run your chosen database. It offers a secure and accessible environment to keep your data safe and ensures scalability, allowing your data operations to grow as needed.

These are the more popular types of database hosting services:

  • Integrated into web hosting
  • VPS
  • Cloud
  • Serverless

Time to explore them by explaining what they are, analyzing how much they cost, and mentioning some key providers.

Built-In Databases in Traditional Web HostingMost traditional web hosting providers offer users the ability to create and manage pre-configured databases like MySQL or PostgreSQL. These databases are directly integrated into the hosting environment. Users can quickly set up them using tools like cPanel. Similarly, they can manage their data with clients like phpMyAdmin.

Save money with our guide on low-cost web hosting alternatives.

  • Cost range: A few dollars per month
  • Some well-known providers: Bluehost, Hostinger, HostGator, InMotion, SiteGround

VPS Database HostingVPS database hosting involves installing databases on a Virtual Private Server. This provides dedicated server space and resources for users to set up their preferred databases. Compared to traditional web hosting, it can result in greater flexibility, performance, and scalability. At the same time, users must configure the environment and manage database updates themselves.

  • Cost range: From a few dollars per month to dozens of dollars per month, depending on how large the VPS is
  • Some well-known providers: DigitalOcean, InterServer, IONOS, Linode, Vultr, Hetzner

Cloud Database HostingCloud database hosting consists of storing databases on virtual servers within a cloud infrastructure, which guarantees high scalability, availability, and agility. Users can easily adjust resources according to changing needs, databases can be accessed from anywhere, and the provider takes care of maintenance, backups, and security for you.

  • Cost range: From a few dollars a month to thousands of dollars a month, depending on usage
  • Some well-known providers: Amazon Web Services RDS, Microsoft Azure, Google Cloud SQL

Serverless Database HostingServerless database hosting is a type of cloud database hosting where users do not have to worry about the underlying infrastructure. The reason is that the cloud provider automatically handles resource allocation and scaling. This simplifies database management, helping developers focus on application development. Users are billed only for the actual usage.

  • Cost range: From a few dollars per month to dozens of dollars per month, based on resources consumed
  • Some well-known providers: AWS Lambda, FaunaDB

Top 5 Inexpensive Options for Database MigrationNow that you are familiar with popular approaches and providers for database hosting, you are ready to see more cost-effective solutions.

If you are eager to learn more about these solutions, check out the summary table below:

| Provider | Databases | Storage | Simultaneous Connections | Read Operations | Write Operations | | --- | --- | --- | --- | --- | --- | | Oracle | Oracle Database, Oracle NoSQL | Up to 20 GB, up to 25 GB per table | Up to 30 sessions | Up to 133 million reads per month | Up to 133 million writes per month | | MongoDB | MongoDB | 512 MB | Up to 500 connections | 100 reads per second | 100 writes per second | | Clever Cloud | PostgreSQL, MySQL | 256 MiB, 10 MiB | Up to 5 connections | — | — | | IBM | IBM Cloudant | 1 GB | — | 20 reads per second | 10 writes per second | | Supabase | PostgreSQL | 500 MB | — | — | — |

Disclaimer: Providers may change these limits and even discontinue free options over time.

Let’s explore the top five database migration options to save money!

Oracle Cloud Always Free TierAfter signing up for Oracle Cloud, new users receive $300 in free credit to use on all eligible Oracle Cloud Infrastructure services for up to 30 days. After that, users can continue on an Always Free plan, which provides more than 25 services available for an unlimited period of time.

The Always Free plan includes two Oracle Autonomous Databases. These serverless databases can be used for transaction processing, data warehousing, Oracle APEX application development, and JSON-based application development. Plus, users gain access to an Oracle NoSQL Database supporting up to 3 tables.

🗃️ Databases: Oracle Database and Oracle NoSQL

📋 Plans:

  • Oracle Autonomous Database:
    • Maximum of 2 databases
    • Up to 20 GB storage per database
    • Up to 30 simultaneous sessions
  • Oracle NoSQL:
    • Up to 133 million reads per month
    • Up to 133 million writes per month
    • Up to 3 tables
    • Up to 25 GB storage per table
    • Read capacity limited to 50 units
    • Write capacity limited to 50 units

👍 Pros:

  • 20+ additional Always Free services (e.g., cloud storage, application performance monitoring)
  • Free forever
  • Seamless upgrade to paid plans at any time
  • Both relational and NoSQL options
  • $300 cloud credit free trial
  • Integration with other Oracle Cloud services

👎 Cons:

  • Payment method required
  • Proprietary Oracle databases
  • Limited to the regions where commercial Oracle Cloud Infrastructure service is available

MongoDB AtlasMongoDB Atlas is a fully managed cloud database service that simplifies deploying, managing, and scaling MongoDB databases. This platform allows users to deploy clusters, which are groups of servers hosting MongoDB databases. These clusters can span multiple cloud regions on providers like Amazon Web Services (AWS), Google Cloud Platform (GCP), and Microsoft Azure.

Users can join MongoDB Atlas for free using the Shared plan, ideal for learning and exploring MongoDB in a cloud environment. The plan includes one M0 free cluster per project, offering 512 MB of storage for a basic MongoDB database.

🗃️ Databases: MongoDB

📋 Plans:

  • M0 free cluster:
    • 512 MB of space
    • Sort in memory limited to 32 MB
    • No more than 100 read/write operations per second
    • Up to 500 simultaneous connections

👍 Pros:

  • Support for AWS, GCP, Microsoft Azure
  • Free forever
  • One M0 free cluster per project
  • Support for MongoDB 7.0
  • Easy upgrade to dedicated clusters
  • No credit card required to start

👎 Cons:

  • Only 512 MB of space available
  • No support for backups, primary failover testing, database auditing, network peering connections, and regional outage testing
  • Free clusters automatically paused after 60 days of inactivity

Clever CloudClever Cloud is a platform-as-a-service (PaaS) provider that automates the scaling, deployment, and management of applications. Their solutions are optimized for several technological runtimes, including Java, Scala, Node.js, and PHP. The provider also offers software-as-a-service (SaaS) solutions dedicated to DevOps projects.

Clever Cloud comes with a robust infrastructure to help developers focus on coding rather than server management. This includes databases, such as Elasticsearch, Redis, PostgreSQL, and MySQL. For PostgreSQL and MySQL, the company has some interesting free plans, though with certain limitations.

🗃️ Databases: PostgreSQL and MySQL

📋 Plans:

  • PostgreSQL:
    • Storage limited to 256 MiB
    • Up to 5 simultaneous connections
    • Shared RAM and vCPUs
  • MySQL:
    • Storage limited to 10 MiB
    • Up to 5 simultaneous connections
    • Shared RAM and vCPUs

👍 Pros:

  • One-click setup, daily backups, and high availability options for PostgreSQL
  • Unlimited number of free databases and applications
  • No payment card required
  • Free credits for testing the service upon registration
  • Cloud services optimized for EU data and privacy regulations

👎 Cons:

  • Limited storage and connections
  • No logs or metrics
  • MySQL plan too limited for real usage

IBM Cloud CloudantYou can create an account for IBM Cloud for free, which gives you access to services like IBM Cloudant. This is a fully managed JSON document database that offers independent serverless scaling of provisioned throughput capacity and storage.

Cloudant is compatible with Apache CouchDB and accessible via an easy-to-use HTTPS API for web, mobile, and IoT applications.

IBM Cloudant is available through a “free forever” Lite plan, limited to 1 GB of storage. Designed for development and evaluation purposes, the Lite plan includes all Cloudant functions but has a limited amount of throughput capacity and data storage.

🗃️ Databases: IBM Cloudant (proprietary NoSQL)

📋 Plans:

  • Lite plan:
    • 1 GB storage
    • Up to 20 reads per second
    • Up to 10 writes per second
    • Up to 5 queries per second

👍 Pros:

  • 99.99% SLA uptime
  • Compliance with GDPR, PCI, SOC2, and ISO 27001
  • All JSON documents are stored across three separate availability zones for high availability and disaster recovery
  • Easy to upgrade to a Standard plan
  • Official SDK available in Java, Node.js, Python, and Go

👎 Cons:

  • Proprietary NoSQL database technology
  • Maximum JSON document size of only 1 MB
  • Only one IBM Cloudant Lite plan instance per IBM Cloud account

SupabaseSupabase is a backend-as-a-service (BaaS) platform. As an open-source Firebase alternative, it provides instant APIs, real-time subscriptions, authentication, data storage, and more. Specifically, it enables users to create new web and mobile application projects with features like a PostgreSQL database, file storage, and authentication out of the box.

Supabase’s FREE plan includes a dedicated PostgreSQL database for up to 2 supported projects. This database offers 500 MB of storage and root access to users, giving them full control over their data.

🗃️ Databases: PostgreSQL

📋 Plans:

  • FREE:
    • 500 MB of storage in a dedicated PostgreSQL database
    • 5 GB of bandwidth per month

👍 Pros:

  • Dedicated PostgreSQL database with no privilege restrictions
  • Possibility to pause and unpause projects at any time
  • Open source technology
  • Active GitHub community forum and Discord community

👎 Cons:

  • Paused after 1 week of inactivity
  • Limited to 2 simultaneous active projects

ConclusionIn this guide, you explored different types of database hosting services and analyzed their costs. Fortunately, there are other modern, low-cost, effective solutions you can consider for database migration. Here, you saw the five best free alternatives to popular database hosting solutions to save money. If you are a small business or individual developer, knowing these affordable solutions can help you save significantly!

The post 5 Low-Cost Database Services to Migrate to appeared first on Semaphore.

View Details

OpenAI is a prominent player in the AI market, experiencing explosive growth. Their top-class AI products use Large Language Models (LLMs) to generate human-like outputs. The fastest and most affordable model is the GPT-4o.

OpenAI offers a powerful API that grants access to its advanced LLMs like GPT-4o, GPT-Turbo, GPT-3.5 Turbo, GPT-3, etc., capable of performing various tasks. These include natural language processing, translation, speech-to-text, image prompts to answer, and much more. In this article, we will see the top emerging OpenAI API alternatives.

Benefits of OpenAI APIOpenAI offers powerful language generation capabilities with numerous other benefits:

  1. Ability to customize the LLM models: The OpenAI API allows fine-tuning options for better and more customized outputs.
  2. Enhanced User Experience: OpenAI API offers a better and more personalized user experience that can engage the user’s experience through different features in its platform.
  3. Ease in Scalability: The API offers you the best and most efficient results while allowing you to scale your product with a large amount of data requests.
  4. Versatility: The API can be applied to diverse tasks such as creating marketing copy, generating product descriptions, and writing different creative text formats.
  5. Improved efficiency and speed: The API is trained such with the large amount of user data that it can execute large and complex problems in minutes and with efficiency.

Why Consider OpenAI Alternatives?While OpenAI API offers you the best, efficient, and accurate results, it may not suit everyone. Let’s see what are some potential drawbacks of OpenAI API:

  • Increased Cost: OpenAI API offers different plans with different pricing. The cost can be prohibitive for some users, who have extensive or continuous needs.
  • Complexity: Not every developer can utilize the complete and proper usage of OpenAI API due to complexity issues. For effective results, good technical expertise is needed to use it properly.
  • Dependence on Data: Because the models effectiveness depends on the data they are trained on, you may see data biases.

Note: As of March 1, 2023, data sent to the OpenAI using their APIs will not be used to train OpenAI models unless you provide the consent explicitly. See more details here.

Benefits of exploring alternativesOpenAI is a leading organization for text generation that offers advanced and powerful models such as GPT-4o. Let’s see the benefits of OpenAI API alternatives:

  • Flexibility: A larger range of APIs enables you to locate one that precisely matches your unique requirements.
  • Cost Savings: Several substitutes have more reasonable price structures, particularly for those with smaller usage volumes.
  • Pay Attention to Particular Needs: Specialized APIs can deliver better performance for particular tasks, providing more value for your specific needs.

Top OpenAI API AlternativesThere are many different options to take into consideration in the field of AI solutions. Here are some of the popular OpenAI API alternatives:

Large Language Models (LLMs) similar to OpenAI’s GPT-3 (Text-based API)Although OpenAI’s GPT-3 and it’s latest versions have been a trailblazing development in large language models (LLMs), several companies have created potent, alternative LLMs.

These models are frequently as good as or better than GPT-3 or GPT-4 in tasks like text generation, summarizing, etc. They also provide their APIs which can be used in your application. Here are some of the best text-based LLMs similar to OpenAI’s GPT-3:

  1. Google Cloud AI APIs:Google Cloud AI provides many different APIs for tasks like text generation, translation, conversational AI, etc. These APIs can be easily integrated into your applications, offering best-in-class performance and are accessible through their Google Cloud Platform (GCP).Google Cloud APIs also provide the features like model customization, scalable infrastructure, and integration with other services. Get your API access here.Cost:While the pricing of APIs differs from usage, Google Cloud gives you the feature of “Pay for what you use”. Also, new customers can get $300 as free credits on their platform. Check out here for more details.
  2. Anthropic Claude API:Anthropic is a US-based research organization that creates reliable and beneficial AI-based systems. Anthropic AI products include the latest Claude 3, which can perform advanced reasoning, code generation, multilingual processing, and much more just like ChatGPT. Claude is a powerful model known for its strong performance.With Claude API, you can build a strong AI solution and scale it with the custom rate limits given in your chosen plan. Get your API access.Cost:Anthropic Claude API offers different pricing plans:
    1. Claude Instant (Input: $0.80 per million Tokens, Output: $2.40 per million Tokens)
    2. Claude 2.0 (Input: $8 per million Tokens, Output: $24 per million Tokens)
    3. Claude 2.1 (Input: $8 per million Tokens, Output: $24 per million Tokens)While the latest Claude 3 plans are different where all models support the vision and give you 200,000 token context windows. Check out the pricing here.
  3. AI21 Labs:AI21 Labs builds the foundation models and advances AI systems for organizations. They have developed innovative tools and APIs that cater to various text-based applications.
    • AI21 Labs offers two different models Foundational models and Task-specific models.
    • They provide you with the best language models, including its flagship model, Jurassic-2 (it is now deprecated and supplanted by Jamba).
    • Their latest top-notch foundation model is Jamba which offers the best quality and performance for your AI needs.AI21 Labs task-specific models are designed to perform specific tasks of the users including the Paraphrase model, Summarize, Text segmentation, RAG contextual answers, and many more.Cost:The AI21 Labs offers two pricing including the “Pay as you Go” and the Custom Plan. The pay-as-you-go plan is best for those who are early in the stage and have restrictions on budget, whereas the custom plan is best for companies who are looking to scale their AI solutions, etc. Check out the complete pricing here.
  4. Cohere:Cohere is a leading enterprise AI platform that offers different models for AI text generation. Cohere provides you the flexible and robust enterprise-grade solutions including organization-specific chatbots, customer feedback, etc.Cohere offers 3 products:
    • Command (Retrieval-augmented generation (RAG))EmbedRerankBelow is an accuracy comparison between Cohere RAG and Claude 3 models for advanced AI applications requiring information from documents and enterprise data sources:

Figure 1: Accuracy comparison between Cohere RAG and Claude 3. Source1. These AI solutions help businesses explore, generate, search for, and act upon information in a new way that’s more intuitive and more natural than ever before. Its language models are customizable as per different cases, giving a better performance.Cost:The pricing of Cohere gives you straightforward pricing plans for their two models. The generative models pricing are as follows: * Command R+ model (Input $3.00 per 1M Tokens, Output $15.00 per 1M Tokens) * Command R model (Input $0.50 per 1M Tokens, Output $1.50 per 1M Tokens) * Command R model (Input $2.00 per 1M Tokens, Output $4.00 per 1M Tokens) fine-tuned.The generative models pricing are as follows: * Rerank 3 model ($2.00 per 1K Searches) * Embed 3 model ($0.10 per 1M Tokens) 2. Hugging Face Transformers:Hugging Face provides a library of pre-trained language models, along with tools for fine-tuning and deploying models. Their models are open-source and can be self-hosted or accessed through an API.Cost:Pricing of hugging face transformers is tailored to individual business needs. Check out the pricing of Huggingfacetransformers API.

When comparing the accuracy of various AI models across different metrics, the latest data reveals significant insights (see below).

Figure 2: Accuracy metrics of various AI models. SourceAccording to Stanford’s HELM accuracy measurements, Claude 3 Opus leads in overall accuracy for MMLU (Massive Multitask Language Understanding- a benchmark used to assess the performance of AI models on a diverse set of tasks) All Subjects with an impressive score of 0.846. Following closely is GPT-4o (2024-05-13), then Google’s Gemini 1.5 Pro (001), and many more.

This comparison highlights the competitive edge of newer models like Claude 3 Opus and GPT-4o in general accuracy, while also showcasing specialized strengths in specific domains like Abstract Algebra by Gemini 1.5 Pro.

Specific AI ServicesApart from all-purpose large language models, many companies provide AI services specifically designed for particular jobs like computer vision, speech recognition, natural language processing, and image production.

To illustrate the capabilities of various AI models, here is a detailed comparison of performance metrics for some of the leading AI services:

Figure 3: Performance comparison of leading AI models across various tasks. SourceLet’s see the specific AI services-based API alternatives to OpenAI:

Text-to-Speech1. Amazon Polly API:Amazon Polly API provides high-quality text-to-speech conversion. It offers high-quality generative lifelike speech from text, as long-form, neural, and high-quality voices both in men’s and women’s voice options. Amazon Polly offers several API operations for integration in your application.Features: * Cloud-based solution * Low latency * Multiple language and voice optionsCost:The Amazon Polly API is a pay-per-use model and there are no setup costs in that. The great thing with Polly is that you can start with your small application and scale later on with ease. See more pricing details for Amazon Polly API.

Computer Vision1. Microsoft Azure Cognitive Services:Azure AI Vision offers innovative computer vision capabilities. With Azure AI Vision you can perform tasks like image recognition, object detection, facial recognition, and text extraction with optical character recognition (OCR). Features: * Facial recognition * Video streaming using Spatial analysis * Image and object detectionCost:Microsoft Azure AI Vision API is a Pay for only what you use with no upfront costs. The pricing model of Azure AI Vision is a pay-as-you-go and calculated based on the number of transactions consumed. See more pricing details.New users can get $200 credits FREE to be used within 30 days. Check it out here. 2. Clarifai API:Clarifai API offers high-quality AI models based on deep learning. The Clarifai API provides a human-like interpretation of video, image, text, and audio. Clarifai API provides customizable computer vision models and a wide range of applications.Features: * Image recognition using their Pre-trained models * Video Analysis using Deep learning models * OCR (Image to Text) and Image labelingCost:Clarifai API offers a user-based pricing model and there are no setup costs in that. You can find their different pricing plans and API costs here.

Machine Learning (ML) and Natural Language Processing (NLP)1. IBM Watson:IBM Watson is a comprehensive platform providing various AI capabilities, including sentiment analysis, question-answering, and machine learning tools. IBM Watson offers different AI models including Watson Natural Language Understanding, Watson Speech to Text and vice versa, watsonx code assistant, etc.IBM Watson APIs facilitate the development of enterprise-class apps that integrate natural language processing capabilities into any hybrid multi-cloud setup. Check out the complete details of IBM Watson APIs here.Features: * Natural language understanding * Best ML models * Language TranslationCost:IBM Watson offers flexible pricing plans depending on the chosen service, including a free tier. Check out the IBM custom products costs here. 2. Google Cloud Natural Language API:Google Cloud Natural Language API is the best natural language AI model that helps you analyze and derive insights from unstructured text. Google Natural Language API allows the customization of the models to classify, extract, and detect sentiment with minimum effort. It also allows us to analyze multi-language text, content classification, and much more.Features: * Sentiment Analysis * Object Recognition * Syntax Analysis (including tokenization and part-of-speech tagging and much more)Cost:Google Cloud Natural Language API offers the pay for what you use and you may also get up to $300 in free credits. Check out the detailed pricing for using the Google Cloud Natural Language API here.

Automatic Speech Recognition1. Google Cloud Speech-to-Text API:Google Cloud Speech-to-Text API allows to conversion of audio into text transcriptions. Using Google’s AI Speech-to-text API, you can easily integrate speech recognition into your applications. It supports the speech in two formats including microphone and file upload.Features: * Advanced speech AI * Transcribe audio using Pre-trained models * Caption videos using AICost:The pricing of Google Cloud Speech-to-Text API is based on the API version, channels, etc methods. For the API version “Speech-to-Text V1 API” pricing is $0.024 per min whereas for “Speech-to-Text V2 API” the cost is $0.016 per min.You may also get up to $300 in free credits here. Check out the detailed pricing for using the Google Cloud Speech-to-Text API here. 2. AssemblyAI APIWith AssemblyAI API you can build the best in industry AI speech models. Assembly API provides high accuracy in speech-to-text and gives support for multiple audio formats including multi-language. The API is easy to integrate and has strong developer support. AssemblyAI has three products: * Speech-to-Text * Streaming Speech-to-Text * Speech UnderstandingFeatures: * Sentiment Analysis * Object Recognition * Syntax Analysis (including tokenization and part-of-speech tagging and much more)Cost:AssemblyAI offers three different pricing plans including the Free tier, pay-as-you-go (start as low as $0.12 per hour for Speech-to-Text), and custom plans according to needs. Check out the detailed pricing here.

Image Generation APIs1. Amazon Titan APIWith the Amazon Titan Image generation API, you can generate high-efficiency images from the text input. Titan API can easily generate realistic, ready-to-use images with high quality in seconds just using natural language prompts. It also allows image editing and image variations.The advanced AI model understands complex instructions with multiple objects and returns studio-quality images that can be used in e-commerce, advertisements, etc. Check out the Amazon Titan Image Generator Demo.Features: * Text-to-image generation * Customization and fine-tuning options * Scalability and IntegrationCost:Amazon Titan API offers different pricing models including One-Demand (pay-as-you-go), batch, etc. Check out the complete pricing details of the Amazon Bedrock Titan API here. 2. DeepAI APIWith the DeepAI AI Image generator API, you can generate images using the text prompts. With the DeepAI API, you can generate three types of images including Standard, HD, and Genius. DeepAI generates high-quality realistic images using text prompts.Features: * Versatile Image Generation * API Customization * Monthly SubscriptionCost:DeepAI Pro is something they offer as a monthly subscription with access to all of their 100+ generative tools and features. Check out the complete pricing details of the DeepAI here. 3. Midjourney APIMidjourney is a cutting-edge tool for AI image creation. The Midjourney API empowers you to seamlessly incorporate Midjourney into your applications, allowing you to generate unique and realistic images based on text prompts.Features: * Realistic image generation * Fine-tuning and customization * Developer community supportCost:Midjourney API offers four different monthly and year plan subscriptions. It includes the Basic plan ($10 per month), the Standard plan ($30 per month), the Pro plan ($60 per month) and the Mega plan ($120 per month). Check out the complete details of Midjourney API pricing.

Choosing the Right AlternativeWhen choosing an alternative to the OpenAI API, consider the following factors:

  • Functionality: Assess the specific capabilities and use cases supported by each alternative to ensure they meet your project’s requirements.
  • Pricing: Compare the pricing models and overall costs of different models to find the most cost-effective one for your budget and usage patterns.
  • Ease of Use: Consider the ease of integration, documentation, and support each provider offers, as it can significantly impact development time and effort.
  • Privacy and Security: Evaluate the privacy and security measures implemented by each provider, especially if your project involves sensitive or regulated data.
  • Project Goals: Ensure that the alternative aligns with your project’s specific goals.

ConclusionIn this article, we have seen the best OpenAI API alternatives. While OpenAI’s API has raised the bar for AI services, there are a lot of other options with more features and advantages to consider as OpenAI alternatives. Businesses and developers can find solutions that better suit their particular needs by looking at these options, accomplishing so may result in more flexibility and cost savings.

A perfect tool for every purpose can be found thanks to the wide range of available AI APIs that are currently available, whether it’s for text generation, image generation, speech recognition, computer vision, natural language processing (NLP), or any other application.

The post OpenAI API Alternatives appeared first on Semaphore.

View Details

When building a software product, whether a framework, library or any interesting piece of technology, having a well-designed documentation website that provides a great user experience is non-negotiable. No matter how exceptional the product itself may be, if the documentation falls short, it can severely impact the adoption and usability of that product. Too often, users complain about wasting time searching for answers, resorting to external resources, or expressing frustration over the minimal effort put into official documentation.

For developers or organizations who have invested countless hours into building a product, such critiques can be disheartening, but they highlight an important reality: if your documentation website is poorly built, it creates a bad impression of the product’s quality.

However, building a software documentation website from scratch can be daunting. It requires great effort and keen attention to detail. Using general-purpose tools like Next.js is certainly a viable option, but it necessitates manually handling aspects such as content management, SEO, accessibility, versioning, and other technical complexities. Dealing with these complexities can divert attention from the core goal of creating comprehensive software documentation.

Over the years, several specialized tools have emerged to address these challenges. One such tool that has gained popularity is Docusaurus. Docusaurus makes building software documentation extremely easy by handling the technical heavy lifting, allowing you to focus more on creating high-quality content instead of worrying about the underlying infrastructure. In this article, we will learn how to use Docusaurus to build visually appealing documentation that is not only functional but also a pleasure to use.

What is Docusaurus?Docusaurus is an open-source tool for building documentation websites. It was created by engineers at Meta to make creating and maintaining documentation a delightful experience. The idea behind Docusaurus is to allow you to write documentation using Markdown and React-based MDX. This combination makes it easy to create both regular documentation pages and interactive content. One of the biggest advantages of Docusaurus is that it is built on top of React, which means you can leverage your existing React knowledge and write custom React components. Many popular companies, such as Algolia, Redis Labs Developer Site, Atlas, Jest, and React Native, to mention just a few, have adopted Docusaurus for their documentation due to the excellent developer experience it provides.

Why Choose Docusaurus? A Comparison with AlternativesDocusaurus is often the popular choice for building documentation websites, but it’s not the only option available. Let’s compare Docusaurus with some popular alternatives:

| Feature | Docusaurus | GitBook | MkDocs | VuePress | Jekyll | Sphinx | | --- | --- | --- | --- | --- | --- | --- | | Base Technology | React | JavaScript | Python | Vue | Ruby | Python | | Customization | High | Moderate | Moderate | High | High | High | | Learning Curve | Moderate | Low | Low | Moderate | Moderate | Steep | | Versioning | Built-in | Limited | Plugin | Plugin | Plugin | Built-in | | Internationalization | Built-in | Built-in | Plugin | Plugin | Plugin | Built-in | | Performance | High | Moderate | High | High | Moderate | High | | MDX Support | Yes | No | No | No | No | No | | Search | Built-in | Built-in | Plugin | Plugin | Plugin | Built-in | | Output Formats | Web | Web, PDF | Web | Web | Web | Web, PDF, ePub | | Best For | Large web docs, React devs | Quick setup, non-devs | Small to medium projects | Vue devs | Static sites | Python projects |

Each tool has its strengths, but Docusaurus shines when you need a modern, customizable documentation website with out-of-the-box features like versioning and internationalization. Its React foundation makes it particularly appealing for teams already working with React-based projects.

PrerequisitesTo ensure you can follow along effectively, this tutorial assumes you have the following:

  • Node.js installed
  • Basic knowledge of React
  • Basic understanding of Markdown syntax

Getting StartedDocusaurus streamlines the process of setting up a documentation website, which allows you to go from concept to deployment quickly. In this section, we will initialize a new Docusaurus project, explore its structure, and get it running locally.

Installing DocusaurusDocusaurus provides a useful CLI tool that scaffolds a new project with sensible defaults. To initialize a new project, open your terminal and run:

npx create-docusaurus@latest my-docs classic This will create a new Docusaurus project named my-docs, but you can use any name you prefer. The classic flag specifies that this project will use the classic template, which includes essential features and configurations for a documentation site. During setup, you will be prompted to choose:

  • Javascript or Typescript (I’ll be using Javascript for the purposes of this tutorial)
  • Optional GitLab/GitHub repository creation

Alternatively, you can use any package manager of your choice like npm, yarn, or pnpm straight from the get-go:

npm init docusaurus# oryarn init docusaurus# orpnpm init docusaurus Project StructureLet’s navigate into the newly created Docusaurus project and get familiarised with its structure:

cd my-docs You will see something like:

my-docs├── blog├── docs│ ├── intro.md│ └── tutorial-basics│ ├── congratulations.md│ ├── create-a-blog-post.md│ ├── create-a-document.md│ ├── create-a-page.md│ ├── deploy-your-site.md│ ├── markdown-features.md│ └── thank-you.md├── package.json├── src│ ├── components│ ├── css│ └── pages├── static├── .gitignore├── barbel.config.js├── package.json├── package-lock.json└── docusaurus.config.js Key directories:

  • blog/: This directory contains blog posts written in Markdown or mdx.
  • docs/: This is where you we will place your documentation files in Markdown or mdx format.
  • src/: Here, you can create pages and customize components, layouts, and other React-related code for your site.
  • static/: This directory is for static assets like images, fonts, etc.
  • docusaurus.config.js: This is the main configuration file for your Docusaurus site.
  • sidebars.js: This file is responsible for auto-generating and customizing the sidebar.

Running LocallyFirst, run npm install to install all the required dependencies, and then start your development server:

npm run start This will compile all the Markdown and React files, start a local development server (typically at http://localhost:3000), and enable hot reloading for instant updates. You should see the default Docusaurus site running in your browser.

Understanding the Config FileWe will begin by exploring the docusaurus.config.js. This file acts as a control center for your Docusaurus project where you can tweak the site’s behavior, appearance, and functionality. Let’s dissect this config file and understand how each part shapes your documentation’s identity and user experience.

Open docusaurus.config.js in your editor. You will see a structure like this:

const config = { title: "My Site", tagline: "Dinosaurs are cool", favicon: "img/favicon.ico", url: "https://your-docusaurus-site.example.com", baseUrl: "/", organizationName: "facebook", projectName: "docusaurus", onBrokenLinks: "throw", onBrokenMarkdownLinks: "warn", i18n: { defaultLocale: "en", locales: ["en"], }, //...};export default config; Core Fields:* title: The title of your Docusaurus site. * tagline: A short tagline or description for your site. * favicon: The path to the favicon icon for your site. * url: The URL where your site will be hosted. * baseUrl: The base URL path for your site (typically /). * organizationName and projectName: These are used for certain Docusaurus features like the edit button. * onBrokenLinks and onBrokenMarkdownLinks: These control how Docusaurus handles broken links. * i18n: This object configures internationalization (i18n) settings like the default locale and supported locales.

Theme ConfigurationThe themeConfig object is where you define your site’s look and feel:

const config = { //... themeConfig: { image: "img/docusaurus-social-card.jpg", navbar: { title: "My Site", logo: { alt: "My Site Logo", src: "img/logo.svg", }, items: [ { type: "docSidebar", sidebarId: "tutorialSidebar", position: "left", label: "Tutorial", }, { to: "/blog", label: "Blog", position: "left" }, { href: "https://github.com/facebook/docusaurus", label: "GitHub", position: "right", }, ], }, footer: { style: "dark", links: [ { title: "Docs", items: [ { label: "Tutorial", to: "/docs/intro", }, ], }, { title: "Community", items: [ { label: "Stack Overflow", href: "https://stackoverflow.com/questions/tagged/docusaurus", }, { label: "Discord", href: "https://discordapp.com/invite/docusaurus", }, { label: "Twitter", href: "https://twitter.com/docusaurus", }, ], }, { title: "More", items: [ { label: "Blog", to: "/blog", }, { label: "GitHub", href: "https://github.com/facebook/docusaurus", }, ], }, ], copyright: `Copyright © ${new Date().getFullYear()} My Project, Inc. Built with Docusaurus.`, }, prism: { theme: prismThemes.github, darkTheme: prismThemes.dracula, }, }, //...}; * navbar: Define logo, title, and navigation items. * footer: Set style, links, and copyright notice. * prism: Choose a syntax highlighting theme.

Presets and ThemesDocusaurus uses presets to bundle plugins and themes:

const config = { //... presets: [ [ "classic", { docs: { sidebarPath: "./sidebars.js", editUrl: "link/to/edit/page", }, blog: { showReadingTime: true, editUrl: "link/to/edit/page", }, theme: { customCss: "./src/css/custom.css", }, }, ], ], //...}; * docs: Set path and sidebar configuration. * blog: Configure blog features. * theme: Apply custom CSS for fine-tuned styling.

This is a quick rundown of the docusaurus.config.js file. The great thing about this file is that you can customize various aspects of your documentation by modifying it, without needing to touch other files or write additional code. For example, If you change the title and tagline to something else, it will update the homepage and other parts where these values are used. The following example demonstrates this:

const config = { title: "Semaphore", tagline: "Build, Test, Deploy - At Lightning Speed", //...}; This will update the homepage title and tagline without having to touch src/pages/index.js directly.

Creating PagesDocusaurus allows you to create standalone pages in the src/pages directory. These pages are different from documentation pages and can be used for custom content such as a homepage, about page, or any other static content you need. To create a new page, navigate to the src/pages folder. If you need to create an about page, for example, you would create about.jsx file inside that folder:

touch src/pages/about.jsx Add content to about.jsx file:

``` import React from "react";import Layout from "@theme/Layout";function About() { return ( About Us Semaphore CI is a cloud-based continuous integration and delivery platform that accelerates software development. We help teams build test, and deploy code faster and more reliably.

   );}export default About;

``` Now visit: http://localhost:3000/about

This code defines a simple React component for the about page. The layout component from Docusaurus provides the site’s header, footer, and other layout features. While it is convenient to use Javascript/JSX to create pages, you also have the option to use Markdown or React MDX.

Add the Page to the NavbarTo make about page accessible via the navigation bar, you need to update the docusaurus.config.js file in the root directory.

const config = { //... themeConfig: { image: "img/docusaurus-social-card.jpg", navbar: { title: "My Site", logo: { alt: "My Site Logo", src: "img/logo.svg", }, items: [ { type: "docSidebar", sidebarId: "tutorialSidebar", position: "left", label: "Tutorial", }, { to: "/blog", label: "Blog", position: "left" }, // Add new Navlink here { to: "about", label: "About", position: "left" }, { href: "https://github.com/facebook/docusaurus", label: "GitHub", position: "right", }, ], }, //... }, //...}; This configuration adds a link to the About page in the navigation bar. Check the browser to see the change.

Creating documentation pageThe documentation content in Docusaurus is typically located in the docs directory of your project. Let’s start fresh by deleting the default files in the docs folder. Next, we will rename the Tutorial navbar item to Docs to better represent a real documentation website. Open the docusaurus.config.js file and locate the themeConfig.navbar.items section. Update the label field of the docSidebar item from Tutorial to Docs:

const config = { themeConfig: { navbar: { items: [ // ... { type: "docSidebar", sidebarId: "tutorialSidebar", position: "left", label: "Docs", }, // ... ], }, },}; To create a new documentation page, make sure you are in docs folder. Create a new Markdown or React MDX file. For this example, let’s create a file named welcome.mdx:

touch docs/welcome.mdx Open welcome.mdx in your text editor and add the following content:

---id: welcometitle: Welcomedescription: This is a description for the welcome pageslug: /welcome---**# Welcome to my Doc**This is my first documentation page in Docusaurus. The new welcome page link would appear in the documentation sidebar. You can also navigate to localhost:3000/docs/welcome in the browser to access the page.

Understanding the structure of a Documentation PageDocusaurus documentation pages consist of two main parts:

  • Front Matter: The top section enclosed in triple dashes (---). It contains metadata like the unique id, title, description, slug, and other details about the page. This metadata guides how Docusaurus generates the sidebar, page titles, URLs, and more. Think of it like the section in HTML. The metadata is not visible in the rendered documentation page but is used by Docusaurus to generate the sidebar navigation, page titles, URLs, and other features.
  • Main Content: This is where you write the actual documentation using Markdown syntax. Here, you can structure the content with headings, code blocks, images, and take advantage of the full expressiveness of Markdown.

Customizing the Documentation PageYou can customize the behavior and appearance of your documentation pages using various front-matter fields. For example, to change the order of a page in the sidebar, you can use the sidebar_position field:

---id: welcometitle: Welcomedescription: This is a description for the welcome pageslug: /welcomesidebar_position: 1---**# Your Markdown content goes here** Setting sidebar_position: 1 will move the page to the first position in the sidebar. Other accepted front matter fields include:

  • sidebar_label: The label displayed in the sidebar navigation for this document.
  • hide_title: A boolean flag to hide the title at the top of the document page.
  • pagination_label: A label for the pagination navigation at the bottom of the document page.
  • custom_edit_url: A custom URL for the “Edit this page” link.
  • keywords: An array of keywords related to the document, used for search engine optimization.
  • image: A relative path or URL to an image that can be used as a preview or social media thumbnail for the document.
  • hide_table_of_contents: A boolean flag to hide the table of contents for the document.
  • toc_min_heading_level: An integer value (1-6) that specifies the minimum heading level to be included in the table of contents.
  • toc_max_heading_level: An integer value (1-6) that specifies the maximum heading level to be included in the table of contents.
  • pagination_next: A label for the “Next” pagination link.
  • pagination_prev: A label for the “Previous” pagination link.

Grouping Documentation PagesYou can group pages in Docusaurus by creating a subfolder inside the docs directory. Let’s say you want to create a “Getting Started” category with pages for “Installation” and “Integration”. First, create the subfolder inside docs directory:

mkdir docs/getting-started Inside the getting-started folder, create a JSON file named _category_.json. This file defines the sidebar navigation structure for the category. Add the following:

{ "label": "Getting Started", "position": 1, "link": { "type": "generated-index" }} The label specifies the category name, and position determines its order in the sidebar. The link field creates an index page for the category, listing all its pages.

Adding Pages to the CategoryNext, create the pages for the category:

touch docs/getting-started/installation.md docs/getting-started/integration.md Add content to installation.md page with appropriate front matter:

---id: installationtitle: Installationsidebar_position: 1---**# Installation**Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Add content to integration.md:

---id: integrationtitle: Integrationsidebar_position: 2---**# Integration**Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Now, these pages will be grouped under the “Getting Started” category in the sidebar, with the index page listing them.

Using React in DocusaurusDocusaurus allows you to embed React components directly in your documentation pages, giving you the flexibility to create interactive and dynamic content. This is possible thanks to MDX (Markdown with JSX), which allows you to use JSX within your Markdown files. This feature is particularly useful when you want to showcase live examples, abstract away repetitive code, or add complex interactivity to your docs. To use React components in your documentation pages, ensure your documentation file format is .mdx instead of .md. For this example, we are going to create a basic component named ColorBox. In your project’s root directory, navigate to the src/componentsfolder (create it if it doesn’t exist) and create a .js or .jsx component:

mkdir -p src/componentstouch src/components/ColorBox.jsx Add content to the file:

import React from 'react';const ColorBox = ({ color }) => ( `);export default ColorBox;` You can then use this component in your MDX file. This example uses the previously created welcome.mdx file for demonstration:

---id: welcometitle: Welcomedescription: This is a description for the welcome pageslug: /welcome---import ColorBox from '@site/src/components/ColorBox';**# Welcome to my Doc**This is my first documentation page in Docusaurus. Note: The @site alias in the import statement is a special path alias in Docusaurus that points to your project’s root directory.

In the browser, you should see the custom component render along with Markdown content:

This approach allows you to create rich and interactive documentation that goes beyond static text and images. You can embed live code editors, interactive diagrams, or even small applications directly in your documentation pages.

Pro Tip: Although React components can greatly enhance your documentation, use them judiciously. For simple content, plain Markdown is often more maintainable and performs better. Reserve React components for interactive elements or complex visualizations that can’t be achieved with standard Markdown.

Versioning DocumentationDocusaurus has built-in support for versioning documentation which allows you to maintain multiple versions of your documentation and provide version-specific content to your users. To enable versioning, you first need to configure your Docusaurus project to support it.

Open docusaurus.config.js and update it to include versioning settings. Add the following to the docs field in the config file:

const config = { //... presets: [ [ "classic", { docs: { // .... lastVersion: "current", versions: { current: { label: "Next", path: "next", }, }, }, //... }, ], ], //...}; This configuration sets up versioning with a Next version, which is the default label for the current version of your documentation.

To create an initial version of your documentation, run the following command:

npm run docusaurus docs:version 1.0.0 This will create a version-1.0.0 directory inside the new versioned_docs directory and a versions.json file in the project directory. You sould have folder structure like this:

my-docs/├── docs/│ └── ... # Current documentation files├── versioned_docs/│ └── version-1.0.0/│ └── ... # Copied documentation files for version 1.0.0├── versions.json # Metadata file for versioning├── ... Creating New VersionsTo add more versions of your documentation, run the docs:version command with the new version number:

npm run docusaurus docs:version 1.1.0 This will create a new directory version-1.1.0 inside the versioned_docs folder, and the documentation files from the docs directory will be copied into it. The new version will also be added to the versions.json file. After creating a new version, you should have an updated folder structure like this:

my-docs/├── docs/│ └── ... # Current documentation files├── versioned_docs/│ ├── version-1.0.0/│ │ └── ... # Documentation files for version 1.0.0│ └── version-1.1.0/│ └── ... # Documentation files for version 1.1.0├── versions.json├── ... You can continue updating the documentation files in the docs directory, and when you are ready to create a new version, simply run the docusaurus docs:version command again with the new version number.

Managing VersionsThe versions.json file in the root directory contains metadata about the different versions of your documentation. It looks like this:

["1.0.0", "1.1.0"] You can manually edit this file to add or remove versions as needed.

Adding Version Dropdown to NavbarAfter setting up versioning, you can update the navbar in docusaurus.config.js to display a version dropdown menu. Open the config file, in config.themeConfig.items, add the following:

const config = { //... themeConfig: { //... items: [ // ... { type: "docsVersionDropdown", position: "right", }, ], }, //...}; This will show a version dropdown menu in the navigation bar.

Pro Tip: If your documentation changes infrequently, versioning may introduce unnecessary complexity. In most cases, maintaining a single, up-to-date version of your documentation is simpler and more effective.

Exploring PluginsDocusaurus has a thriving ecosystem of plugins that can extend the functionality of your documentation site. Let’s look at an example of integrating Translation and Internationalization plugins in Docusaurus.

Translation and InternationalizationDocusaurus has built-in support for internationalization (i18n) that allows you to create a multi-language documentation website. This feature enables you to reach a global audience by providing translated content in multiple languages. Let’s set up and manage translations in Docusaurus project.

Configuring LocalesThe first step is to configure the locales (languages) you want to support in your documentation site. Open the docusaurus.config.js file and locate the i18n object. By default, it should look like this:

i18n: { defaultLocale: 'en', locales: ['en'], }, To add a new locale, for example, French (fr), update the locales array:

i18n: { defaultLocale: 'en', locales: ['en', 'fr'], }, Translating ContentAfter configuring the locales, you can start translating your content. Docusaurus expects translations to be placed in a specific directory structure within the project-root/i18n folder. Create a directory named i18n in the root folder:

mkdir -p i18n Docusaurus provides a CLI that you can use to manage translations. To extract the translatable content from your Markdown and React components, run the following command

npm run write-translations This command, by default, will generate only the English (en) locale directory along with translatable JSON files inside the i18n directory. To instruct the command to generate a French locale or your desired locale, you will have to specify it by providing the --locale flag. For example, to generate the French (fr) locale, run:

npm run write-translations -- --locale fr This will generate the fr locale directory containing translatable files inside the i18n directory. You should have a folder structure like this:

my-docs/└── i18n ├── en │ ├── code.json │ ├── docusaurus-plugin-content-blog │ │ └── options.json │ ├── docusaurus-plugin-content-docs │ │ ├── current.json │ │ ├── version-1.0.0.json │ │ └── version-1.1.0.json │ └── docusaurus-theme-classic │ ├── footer.json │ └── navbar.json ├── fr │ ├── code.json │ ├── docusaurus-plugin-content-blog │ │ └── options.json │ ├── docusaurus-plugin-content-docs │ │ ├── current │ │ │ ├── version-1.0.0 │ │ ├── current.json │ │ ├── version-1.0.0.json │ │ └── version-1.1.0.json │ └── docusaurus-theme-classic │ ├── footer.json │ └── navbar.json //.... Every JSON file in the locale represents a specific aspect of your documentation where you can manually translate the content. For instance, current.json is responsible for translating general text content and UI elements like the site title, description, and theme labels. The navbar.json and footer.json in the i18n/fr/docusaurus-theme-classic directory are where you translate the navbarand footer content to French, respectively. You can explore all the JSON files and translate as many aspects as you want to create a fully localized documentation website.

To translate your documentation content, you have to copy your documentation content from the docs files to i18n/fr/docusaurus-plugin-content-docs/current/. Copy all the files inside the docs directory:

mkdir -p i18n/fr/docusaurus-plugin-content-docs/current cp -r docs/* i18n/fr/docusaurus-plugin-content-docs/current/ After copying, you have to manually translate the content to your desired language, which in this case is French. The structure for the multilingual setup should look like this:

my-docs/└── i18n ├── en │ ├── code.json │ ├── docusaurus-plugin-content-blog │ │ └── options.json │ ├── docusaurus-plugin-content-docs │ │ ├── current.json │ │ ├── version-1.0.0.json │ │ └── version-1.1.0.json │ └── docusaurus-theme-classic │ ├── footer.json │ └── navbar.json ├── fr │ ├── code.json │ ├── docusaurus-plugin-content-blog │ │ └── options.json │ ├── docusaurus-plugin-content-docs │ │ ├── current │ │ │ ├── getting-started │ │ │ │ ├── installation.md │ │ │ │ ├── integration.md │ │ │ ├── welcome.mdx │ │ │ ├── version-1.0.0 │ │ ├── current.json │ │ ├── version-1.0.0.json │ │ └── version-1.1.0.json │ └── docusaurus-theme-classic │ ├── footer.json │ └── navbar.json For transaltion to take effect, you will need to restart your server and add the --locale flag.

npm run start -- --locale fr Now, when you refresh the browser, you should see something like this when you visit localhost:3000/fr/docs/next/welcome:

Enabling the Language DropdownAfter configuring the locales and providing translations, you can enable a language dropdown in your site’s navigation bar. Open docusaurus.config.js file and locate the themeConfig.navbar.items array. Add the following item:

const config = { themeConfig: { navbar: { items: [ // ... { type: "localeDropdown", position: "right", }, ], }, },}; This will add a language dropdown to the right side of the navigation bar, allowing users to switch between the configured locales.

Routing and URL StructureNote: Internationalization works a little differently in the dev environment compared to production. In production, you can seamlessly switch between the provided languages without any issues. However, in the development environment, you need to specify the locale when starting the server, and it only supports one language at a time. This means if you start your dev server in the French locale and switch to, say, English, you will get a 404 error. In production, this will not be an issue as Docusaurus automatically handles the routing and URL structure for your translated content. When a user selects a different locale from the language dropdown, Docusaurus will load the corresponding translated content and update the URL to reflect the selected locale. For example, if a user visits the /docs/welcome URL on the English (en) site, and then switches to the French (fr) locale, Docusaurus will load the French translation of the welcome.mdxfile and update the URL to /fr/docs/welcome.

More about about PluginsEarlier, we learned how to leverage the localization plugin, but that is just a small taste of what plugins can do for your Docusaurus site. There are tons of useful plugins that make things easier. Let’s say you need to integrate Google Analytics into your project. Here is how:

Install the PluginFirst, install the required plugin by running this command:

npm install --save @docusaurus/plugin-google-gtag With the plugin installed, you can configure it in your docusaurus.config.js file. Look for the presets array and find the presets entry. Then add this gtag config inside it:

const config = { //... presets: [ [ 'classic', /** @type {import('@docusaurus/preset-classic').Options} */ ({ gtag: { trackingID: 'G-999X9XX9XX', anonymizeIP: true, }, docs: { sidebarPath: './sidebars.js', lastVersion: 'current', versions: { current: { label: 'Next', path: 'next', }, }, }, //... }), ], ], //...}; Make sure to swap out G-999X9XX9XX with your actual Google Analytics tracking ID. The anonymizeIP option helps with privacy by anonymizing visitor IP addresses before sending data. Google Tag is disabled in the development environment. This behavior is intentional to prevent unnecessary analytics data from being sent while you’re developing and testing your site locally. Google Tag will be enabled in the production environment and your site should be able to collect analytics data. You can use tagassistant.google to confirm if your Google Analytics implementation is working correctly using your production link.

You can explore more plugins from Docusaurus official website to extend the capabilities and functionality of your documentation website.

Deploying Your Docusaurus WebsiteAfter building your documentation website, there is nothing more satisfying than seeing it live and running. Docusaurus provides a CLI to streamline deployment, making the process straightforward. You can also deploy your site to cloud services like Vercel or Netlify, which offer additional features and ease of use.

Using Docusaurus CLI for DeploymentDocusaurus includes a built-in command-line tool that simplifies the deployment process. To deploy your site using the Docusaurus CLI:

Ensure that your site is built and ready for deployment. Run the following command to create a production-ready build:

npm run build This will generate static files in the build directory, which can be served by any static site hosting service.

To ensure everything works as intended, test your build locally by running the following command:

npm run serve This will start a local server and you can view your site by navigating to the provided URL. Make sure to check all pages and features to verify that they are functioning correctly.

Deploying to Cloud ServicesFor more flexibility, deploy your Docusaurus site to cloud services like Vercel or Netlify, which offer seamless integration with Git and automated deployment workflows. Follow their setup guides:

  • Deploy to Vercel
  • Deploy to Netlify

Pro Tip: Fix broken links in your Docusaurus project to prevent deployment failures.Earlier in this tutorial, we deleted all default files and folders in the docs directory. This may have created broken links, particularly references to docs/intro. To ensure smooth deployment:

  1. Open docusaurus.config.js
  2. Navigate to config.themeConfig.footer.links.items
  3. Delete or Replace /docs/intro with a working link or use the relative path /.

const config = { //... themeConfig: { footer: { style: "dark", links: [ { title: "Docs", items: [ { label: "Tutorial", to: "/docs/intro", // Resolve this broken link }, ], }, //... ], }, }, //...}; Repeat this process for src/pages/index.js, updating any similar references.

Wrapping UpWe have covered a lot of ground in this guide. We touched on key concepts such as initializing a new Docusaurus project, creating documentation, versioning, integrating plugins, deploying a Docusaurus website, and more. Building a documentation website is not a simple task, but tools like Docusaurus take a lot of that complexity out of the equation by providing streamlined workflows and essential features out of the box. While this guide aims to provide a solid foundation, there is still much more to explore with Docusaurus. There are limitless possibilities for customization through Docusaurus’ extensive ecosystem of plugins and themes.

Feel free to clone my working repository in parallel to the fresh, walk-thru provided here. Please do reach out to me for any questions/clarifications or bugfixes!

The post Using Docusaurus to Build A Modern Documentation Website appeared first on Semaphore.

View Details

Conventional data analysis workflows can be cumbersome and require keeping up with the exponential volume and complexity of modern data sources, which may result in significant hurdles, such as being time-consuming, error-prone, and requiring specialized expertise. Automating these processes can significantly reduce the time and resources needed for data analysis.

Retrieval-augmented generation (RAG)-enabled large language models (LLMs) offer a trivial solution to the challenges of conventional data analysis workflows. By combining the capabilities of natural language processing (NLP) with the ability to retrieve and integrate relevant information from external sources into its response, RAG-enabled LLMs can access and process vast amounts of structured, semi-structured, and unstructured data. Organizations can automate various data analysis tasks by leveraging RAG-enabled LLMs, including summarising data trends, identifying correlations and patterns, and detecting anomalies. With RAG-enabled LLMs, the time and effort required for data analysis are significantly reduced while accuracy and efficiency increase.

Here, you’ll explore the possibilities of using RAG-enabled LLMs to automate data analysis and how it transforms the way we work with data.

Prerequisites Understanding of Python, Data Analysis, and How Large Language Models Work. * An OpenAI account and API key for accessing their LLMs * You can find the data required to follow along on the GitHub repository. * Development Environment*: Create and activate a virtual environment with Python in your workspace directory. Then, proceed to install the required packages.

python -m venv .venv && source .venv/bin/activatepip3 install pypdf python-dotenv pandas beautifulsoup4 llama-index==0.9.48 What are RAG-Enabled LLMs?Large language models (LLMs) have become robust tools for processing natural language. Due to continuous increases in their context window, they can fluently comprehend, produce, and modify human language.

The architecture of LLMs is a crucial factor in their versatility. It allows data from external sources to be seamlessly integrated, making LLMs a base or kernel for building on top of.

Connection to external data enables the development of RAG-enabled LLMs, a prevalent paradigm for fitting external data into LLMs.

Retrieval-Augmented Generation (RAG)One primary reason for RAG is that LLMs do not have access to all or most of the data you might care about, such as private data or data generated after training the LLM.

RAG combines the strengths of both generative and retrieval-based models. Its architecture consists of multiple components, including Query transformations, Routing, Query construction, Indexing, Retrieval, and Generation. The focus here will be on the last three:

  • Indexing: This involves creating an index or a searchable database of a collection of related data or documents after chunking and embedding them. The indexing mechanism enhances the model’s retrieval of accurate information.
  • Retrieval: Unlike traditional LLMs, RAG incorporates a retrieval mechanism responsible for retrieving relevant information from external knowledge sources, such as text databases or pre-indexed corpora. This mechanism serves as additional context for the generative component.
  • Generation: This component functions similarly to conventional LLMs, generating text based on input prompts and internal knowledge.

The unique architecture sets RAG-LLMs apart, making them a powerful tool in data analysis.

Overcoming Limitations of Conventional LLMsA common issue with conventional LLMs is their limited access to data, limited knowledge representation, and hallucinations, which result in unrelated or irrelevant responses. By integrating external knowledge, RAG-enabled LLMs overcome these difficulties and give the model access to more recent information beyond its pre-training data, allowing for more coherent, relevant, and factually accurate outputs.

Role of Vector Databases in RAG SystemsVector databases play a crucial role in RAG systems by facilitating efficient retrieval of relevant information. These databases organize textual information into high-dimensional vector representations, enabling fast and accurate similarity searches. Vector representations capture semantic relationships between documents, allowing RAG models to retrieve contextually relevant information for a given query prompt.

Applications of RAG-Enabled LLMs in Data AnalysisYou’ll see how to integrate external data with OpenAI’s LLMs to implement RAG-enabled data analysis in practice. You’ll work with structured (JSON file containing countries’ GDP for 2023), semi-structured (HTML containing info about a country’s economy), and unstructured (PDF containing info about a country’s economy) data.

In your project directory, create the following files and directories:

.├── data│ ├── Economy_of_China.html│ ├── Economy_of_Nigeria.pdf│ ├── gdp_2023.json└── .env└── main.py1 directory, 5 files The main.py file is the project’s entry point, the data folder holds project-related data and the .env file holds the API Key (OPENAI_API_KEY=<key>).

Working With Structured DataStructured data comes in a predefined manner and adheres to a specific schema, making it easy to analyze. Here, you’ll work with JSON data loaded into a pandas dataframe

Add the following code to the main.py file.

```

import the required libraries (1) import osimport pandas as pdfrom llama_index.query_engine import PandasQueryEnginefrom llama_index.tools import QueryEngineTool, ToolMetadatafrom llama_index.agent import ReActAgentfrom llama_index.llms import OpenAIfrom dotenv import load_dotenv## load .env file (2)load_dotenv()## construct file path (3)gdp_file_path = os.path.join("data", "gdp_2023.json")## load data (4)gdp_df = df = pd.read_json( gdp_file_path, orient="records", dtype={"gdp": int}, convert_dates=["year"],)## create query engine (5)gdp_query_engine = PandasQueryEngine(df=gdp_df, verbose=True)## list of query engine tools (6)tools = [ QueryEngineTool( query_engine=gdp_query_engine, metadata=ToolMetadata( name="countries_gdp_ata", description="This provides data regarding the GDP of nations.", ), ),]

``` The code sets up a query engine for the GDP data loaded into a pandas dataframe and defines a tool for querying the data.

Here, we use the PandasQueryEngine as a wrapper interface for the gdp_df dataset to make a query engine (gdp_query_engine). This way, we have an interface that allows asking questions about GDP data using natural language. Using the right query engine, you can create a generic interface for any data you want to interact with.

Using a Python list (tools), we define a list of QueryEngineTool instances, which represent tools for querying the knowledge graph of the external data. QueryEngineTool wraps the gdp_query_engine with metadata. You can have more than one query engine tool. The agent accesses and picks the right tool for a task.

Add the following code to the main.py file below the existing code.

```

load llm and create agent (7)llm = OpenAI(model="gpt-4")agent = ReActAgent.from_tools(tools, llm=llm, verbose=True)## take input prompt (8)while (prompt := input("Enter a prompt (q to quit): ")) != "q": result = agent.query(prompt) print(result)

``` The code creates an agent with a GPT-4 language model and enters a loop that prompts the user for input.

You can use any Large Language Model of your choice by importing the appropriate model class and passing the model’s name.

Using the ReActAgent class, you create an agent to access and interact with the list of query engine tools. The from_tools method initializes the agent with a list of tools and the LLM instance.

Execute the main.py file. As a prompt, input “Which region has the highest GDP?”.

Note how the agent uses the tool we provided in the code to attempt to answer the question asked.

Also, note how the agent takes the necessary steps to answer the question correctly.

Using a Prompt TemplateYou can use a prompt template to provide additional information on processing prompts and queries, provide context, specify the task, guide the response, reduce ambiguity, and improve accuracy. If one is not provided, the default prompt template is used.

Create a prompts.py file in your project directory with the following code.

from llama_index import PromptTemplateinstruction_str = """\ 1. Convert the query to executable Python code using Pandas. 2. The final line of code should be a Python expression that can be called with the `eval()` function. 3. The code should represent a solution to the query. 4. PRINT ONLY THE EXPRESSION. 5. Do not quote the expression."""new_prompt = PromptTemplate( """\ You are working with a pandas dataframe in Python. The name of the dataframe is `gdp_df`. This is the result of `print(gdp_df.head())`: {gdp_df_str} Follow these instructions: {instruction_str} Query: {query_str} Expression: """) You can embed instructions, prompts, and queries within a prompt template, optimizing the input to the agent and response from the agent.

Update the initialization of gdp_query_engine in the main.py file to include an instruction_str:

from prompts import new_prompt, instruction_str......## create query engine (5)gdp_query_engine = PandasQueryEngine( df=gdp_df, verbose=True, instruction_str=instruction_str)gdp_query_engine.update_prompts({"pandas_prompt": new_prompt}) This way, you’ll have provided additional instructions and updated the prompts template.

Execute the main.py file. As a prompt, input “Which country has the highest GDP and which region does it belong?”.

With this, you can customize your prompt templates to fit your data analysis needs and requirements.

Working With Semi-Structured and Unstructured DataSemi-structured data is information that doesn’t necessarily reside in a database but still has some organizational properties that make it easier to analyze. Unstructured data presents a significant challenge in data analysis due to its lack of a predefined data model.

Here, you’ll see how to build an agent to analyze semi-structured data (HTML containing information about a country’s economy) and unstructured data (PDF containing information about a country’s economy) using natural language processing (NLP) techniques, embedding, vectorization, and indexing.

Create a readers.py in your project directory with the following code.

import osfrom llama_index import StorageContext, VectorStoreIndex, load_index_from_storagefrom llama_index.readers import PDFReader, HTMLTagReaderload_dotenv()def create_and_persist_index(data, index_name): print("generating index...", index_name) index = VectorStoreIndex.from_documents(data, show_progress=True) index.storage_context.persist(persist_dir=index_name) return indexdef load_existing_index(index_name): storage_context = StorageContext.from_defaults(persist_dir=index_name) return load_index_from_storage(storage_context)def get_index(data, index_name): if not os.path.exists(index_name): return create_and_persist_index(data, index_name) else: return load_existing_index(index_name)html_path = os.path.join("data", "Economy_of_China.html")china_html = HTMLTagReader(tag="main").load_data(file=html_path)china_index = get_index(china_html, "china")china_engine = china_index.as_query_engine()pdf_path = os.path.join("data", "Economy_of_Nigeria.pdf")nigeria_pdf = PDFReader().load_data(file=pdf_path)nigeria_index = get_index(nigeria_pdf, "nigeria")nigeria_engine = nigeria_index.as_query_engine() The code creates and manages a vector-based store and search index for the HTML and PDF documents. A vector-based store index is a type of data structure and retrieval system that uses vector representations of data to perform searches. This method relies on embedding data into high-dimensional vectors of similar dimensionality that capture the semantic essence of the content, allowing for more subtle and refined searching.

  • create_and_persist_index creates a new index from the provided documents using VectorStoreIndex.from_documents, persists the created index to the specified directory (persist_dir=index_name) and returns the index.
  • load_existing_index makes a storage context object (storage_context) from the specified directory (persist_dir=index_name) using StorageContext.from_defaults, loads the index from the storage context using load_index_from_storage and returns it.
  • get_index checks if an index already exists by looking for a directory with the name index_name. It loads and returns an index if it exists or creates and persists one if it doesn’t.
  • As a final step, the documents are being processed. The file paths to the documents are defined. The documents are then read using the appropriate readers (HTMLTagReader and PDFReader). Llama Hub provides different kinds of readers to suit your needs. get_index is called to either create a new index or load an existing one. Lastly, the index is converted to a query engine.

The list of query engine tools in main.py needs to be updated to contain the china_engine and nigerian_engine query engines.

Add the following code to the main.py file.

from readers import china_engine, nigeria_engine......tools = [ ... ... QueryEngineTool( query_engine=nigeria_engine, metadata=ToolMetadata( name="nigeria_economy_data", description="this gives information about the economy of the Federal Republic of Nigeria", ), ), QueryEngineTool( query_engine=china_engine, metadata=ToolMetadata( name="china_economy_data", description="this gives information about the economy of the People's Republic of China", ), ),] Execute the main.py file. It takes a few minutes to generate the embedding, knowledge graph, and index for the documents and store them in the specified folders. This would not be required in subsequent runs except if deleted.

As a prompt, input “What is the difference between the economy of China and Nigeria?”.

As seen, the agent makes use of the tools provided to guide its thoughts and answer the question.

Enhancing the Capabilities of the AgentLlamaIndex provides a FunctionTool class that allows you to wrap a Python function and turn it into a reusable tool or engine.

Let’s implement utility functions that download images from the web and take notes for the user.

Create a utils.py in your project directory with the following code.

import osimport requestsfrom llama_index.tools import FunctionTooldef download_image(url: str): file_name = url.split("/")[-1] response = requests.get(url) response.raise_for_status() file_path = os.path.join("data", file_name) with open(file_path, "wb") as file: file.write(response.content) return file_pathdef save_note_to_file(note: str): note_file_path = os.path.join("data", "notes.txt") if not os.path.isfile(note_file_path): with open(note_file_path, "w"): pass with open(note_file_path, "a") as file: file.write(note + "\n") return notedownlod_engine = FunctionTool.from_defaults( fn=download_image, name="download_image", description="This tool allows the user to download images at a specified URL",)note_engine = FunctionTool.from_defaults( fn=save_note_to_file, name="make_note", description="This tool allows the user to save a text note to a file.",) The list of query engine tools in main.py needs to be updated to contain the downlod_engine and note_engine tools.

from utils import downlod_engine, note_engine......tools = [ downlod_engine, note_engine, ... ... ...] Execute the main.py with the prompts shown in the images below.

A notes.txt file containing the agent’s notes is created in the data folder.

Similar to taking notes, the agent can download an image at a given URL.

You should see an image file, WxHixX4.jpg, created in the data folder.

This way, you can create utility functions (tools) that can perform various tasks, such as data processing, calculations, or interactions with external systems.

ConclusionHere, we’ve covered the concept of RAG-enabled LLMs for data analysis. RAG-enabled LLMs mark a new frontier in leveraging the power of language models for data-driven tasks. By blending LLMs’ natural language processing skills with retrieval methods, RAG models provide a new way to automate data analysis. This combination enables companies to manage datasets of different structures more effectively, uncover valuable insights with less human involvement, and make data analysis available to users of varying skill levels.

The post Using RAG-Enabled LLMs to Automate Data Analysis appeared first on Semaphore.

View Details

Imagine coding side by side with a tireless teammate, who whispers brilliant Ideas, and suggestions in your ear. That’s the luxury GitHub Copilot provides. GitHub Copilot has provided us with relevant code suggestions, and developer assistance, with other helpful features like suggesting code snippets, comments, and documentation based on patterns found in open-source repositories, all while mimicking your coding style.

But here’s the issue: Copilot comes with a monthly subscription fee. If budget is a major concern and you still want to keep up with such a high standard of service, there are free alternatives to consider. One strong contender that comes to mind is Codeium, which offers many of the same code completion and suggestion features as Copilot, with no cost at all.

GoalsIn this article, we will:

  • Explore Codeium in detail.
  • Provide a step-by-step guide for successful installation.
  • Explain how to set up Semaphore with Codeium.
  • Highlight other key functionalities and uses.
  • Compare Codeium free indivdual plan with GitHub Copilot paid individual plan.
  • Discuss how best to use Codeium.

What is Codeium?Codeium is a code completion advanced AI tool, that offers out-of-the-box free toolkits for personal, team, and company usability. With its advancement, Codeium accelerates coding processes, boasting of completing codes in over 70 languages.

Features of CodeiumWe could debate about AI taking our jobs all day, but there is a strong case for its huge added advantage to our productivity as developers, and Codeium hasn’t fallen short of these qualities. With Codeium you have access to;

  • Limitless single and multi-line code completions indefinitely.
  • Support for over 70 programming languages.
  • Seamlessly integrate into all leading IDEs and web editors, including VS-code, Colab, and Jupyter Notebooks.
  • Receive assistance via Discord Community.
  • Ultra-fast response
  • Chatbot for AI suggestions
  • Fix code errors, within our IDE
  • End-to-end data encryption

Differences Between Codeium and Github CopilotCodeium draws a lot of similarities from Github, but it has its improved uniqueness. We will be comparing Github Copilot’s individual paid plan to Codeiums free individual plan;

PriceCopilot operates on a subscription-based model, priced at approximately $10 per month or $100 per year. Codeium’s Individual plan is entirely free and remains so indefinitely, offering users access to its full range of features without any cost.

FunctionalityCopilot functionality rallies around Single multi-line codegen and chat, on the other hand, Codeium shares the same thing with an added feature of In-IDE integrated search. This integration helps the coding process by allowing developers to have a humanlike convo with the AI or search for additional resources without switching between multiple applications.

Supported IDESCopilot has support for the following IDEs; VSCode, JetBrains, Vim/Neovim, Visual Studio, and Godellum. While Codeium supports; VSCode, JetBrains, Visual Studio, Jupyter/Colab/Deepnote/, Databricks Notebooks, Chrome, Vim/Neovim, Emacs, Eclipse, Sublime Text, and VSCode Web IDEs (ex. Gitpod).

Support for LanguagesGitHub Copilot and Codeium both offer built-in support for popular languages like Python, Javascript, TypeScript, Ruby, Go, Java, PHP, C++, C, Rust, and Swift. However, Codeium provides an added advantage with its large support for emerging or lesser-known languages of over 70+ of these languages.

DeploymentCodeium can be used online, installed on your servers, or within a private network. They’ve passed a security check called SOC 2 Type 2 for their online version. On the other hand, GitHub Copilot has not put out a statement on its security check yet. This could mean that Codeium has been proven to be secure when used online.

InstallationTo have a developer experience we must first install this software. In terms of installing this application, there are usually two ways we can install codeium;

  • Playground
  • Get Extension

PlaygroundThe first method we will explore will be “trying this out in our browser”:

With this, you can go on a the playground and try codeium in your browser.

Enough of the talks, let’s see how Codeium works, Javascript will be a better choice for me, cause that is what I prefer, you could use another language of preference, and still get the same result. I made edits to the playground, and made a comment that reads “Write a quick todo app in Js”:

Then I went ahead to the next line, and immediately suggestions were given;

You will only need to click tabs to accept suggestions:

Voila! We have our variables to get started. The usefulness of the playground is to convince you to install it in your IDE, and that takes us to the second form of installation

Get ExtensionTo get the extension running in your code editor click on the “get extension” as highlighted below:

The tab below will be open, choose your preferred code editor, there about over 20 code editors available, in this case, we will be using Visual Code Editor for illustration purposes.

When you click your code editor you will be asked to sign up, after signing up you will be able to see the display below, click on a quick install, and it will take you to Visual Studio Code extensions section, follow all instructions and you will have it successfully installed. If you successfully had Codeium’s extension installed your VS code should look like this:

If you missed out on anything during the installation, when you click on the Codeium icon at the left side of your VS code, you will get an error, this is because you may have missed out on linking Codeium to your IDE properly, to fix that, navigate to your Codeium profile, the token will be provided for you there.

To finish up, copy the token, and go to VS Code. In VSCode, open the Command Palette (Ctrl/Cmd + Shift + P), type Codeium: Provide Authentication Token, and hit Enter. This will initiate the authentication process.

Exploring Codeium’s BenefitsThere are various benefits that come with codeium, these benefits are;

Limitless single and multi-line code completions.Codeium can suggest an entire line or even multi-line code blocks. This means Codeium can potentially complete logic structures, or functions without any limitations on the amount of code it proposes.

Imagine you’re building a music recommendation application. Codeium could suggest the entire array of songs, artists, genres, and moods, including the initialization, condition, and body, all at once. Let’s put this to a test:

Single Line:

Multiple Line:

This is the complete code it generated for that function:

Codeium ChatbotCodeium chat can be used to generate boilerplates, explain Code, refactoring Code, and editing code. One fun thing you need to know is that the Chatbot is also called Codeium university, and this is because you can go on an on having a very interactive learning session with this AI.

Generate Boilerplates:

You can easily prompt Codeium to Generate a HTML boilerplate for example and it does this for you. Feel free to copy or Insert directly into your code:

Explanation, Refracturing, and Editing:

To leverage this chatbot feature you can easily select the code block you want to understand or improve. You can highlight any code by dragging your mouse over it, and then right-clicking on the highlighted code block.

This will reveal a context menu with various options, as seen below go down and select either Codeium: refactor selected code or Codeium: explain selected code.

To enjoy Codeium edits, you will also need to highlight the code and then press ctrl + I, this will open a tab like the one below, feel free to prompt as you wish.

Intelligent Search:

Codeium Search helps you find stuff in your code! It currently works with Python, JavaScript, TypeScript, and Go. Here’s how to get started: Go to the Codeium chat prompt it to search your code with natural language, explaining what you want and it will do so!

Downside: Indexing large amounts of code files can take time and slow down your computer, it happens once so no need to fear.

Unit Test and API Integration Made EasyCodeium has an extensive knowledge of various APIs and can seamlessly insert them into your code, right from your IDE. It doesn’t stop there, it can as well generate unit test for your code.

Help developers LearnUsing Codeium you could cut your learning curve so fast. Let’s say for instance you are stuck on a concept or function. Codeium can explain code snippets and logic structures clearly.

Also, the Contextual Code Completion helps with both basic and complex completions. This can expose you to new coding patterns and approaches in such a short time, mind you since it’s trained on a lot of data it teaches you a widely accepted pattern and good code practices.

Product reviewSetting up Semaphore with CodeiumTo set up a Semaphore CI build for a web application with Codeium, let’s get a semaphore account. I prefer using GitHub for my authentications, I will advise you to do so. For this exercise, we will consider a simple example of a Node.js web application using Express.js as the framework that renders Hello, Semaphore, Our build works Now! On the browser(LocalHost).

We can go ahead to create a folder named “semaphorebuild”, doesn’t need to be the best of names. Navigate into the project, Install Express.js, and create an App.js file in the root of our project. Here’s a basic file structure:

Project file structure;

-/semaphorebuild - app.js - package-lock.json - package.json In the app.js file, copy and paste the code below:

const express = require('express');const app = express();app.get('/', (req, res) => { res.send('Hello, Semaphore, Our build works Now!');});const PORT = process.env.PORT || 3000;app.listen(PORT, () => { console.log(`Server is running on port ${PORT}`);}); You can run this using node app.js. In the root of our project, we will create a .semaphore folder, and in it, we will create semaphore.yml file. This file contains the instructions for Semaphore on how to build, test, and deploy our application. We will be prompting Codeium to help with this. Here is the prompt:

Codeuim has given us a template, we can now insert it in our semaphore.yml file:

In our .yml file we need to make a few changes, and here is why; The code template above will only spin up a VM and echo a string “Hello, Semaphore”.

We could take a step further to build it, start the App.js env, and test with a curl cmd that it’s actually up & running.

We want to add three key changes;

  • Node.js Installation: This script uses sem-version node to switch to a specific Node.js version.
  • Caching: We will use Semaphore’s caching mechanism to speed up the installation of node modules.
  • Testing: After starting the server, we will need to perform a curl request to the localHost:3000 route, to be sure it’s up and running.

Feel free to replace the code in the .yml file with this code.

Going further, we will create a repo in GitHub and push the code there. To test our build, navigate to Semaphore Home page.

Create a new project, and follow the provided steps to get your newly created repo linked to Semaphore.

Choose your repo:

Semaphore automatically initializes your project, and check if you have a semaphore.yml file:

Select the “I will use the existing configuration” option above, as Codeium has provided us with that already. You will have to trigger a build by a more recent push to the repository, afterwards, Semaphore builds and gives its results really fast:

With this basic example, we have been able to see how Codeium can help in a real-world scenario, here is the GitHub repo.

Frequently Asked QuestionsWhen thinking about what Codeium can do and why it might be helpful, it’s crucial to realize that it could be used incorrectly or not fully understood. New users might have questions about how to use it properly.

Here are some frequently asked questions to consider, along with answers, to emphasize the importance of using Codeium effectively.

How should developers approach using Codeium, considering it’s an AI?Codeium is an AI tool and should be used with a certain mindset, it’s important to note that Codeium is not a replacement for your coding skills and the critical logic that comes with it.

Use it effectively to boost your productivity and efficiency, but always maintain a focused eye on its suggestions and ensure they align with your coding best practices. While Codeium’s code suggestions are mostly correct, it’s important to remember that it’s an AI tool and may sometimes generate incorrect, repetitive, or instinctive code.

How does Codeium use artificial intelligence to assist with coding?From close experience, Codeium uses AI in two different approaches to become a good coding friend:

  • Code Analysis: Codeium utilizes pre-trained AI models that have been fed lots of public code. This training allows them to understand programming language structures, common coding patterns developers use, and even best practices.
  • Real-time AI Assistance: Whenever you code, Codeium puts this knowledge to work, by analyzing what you’ve typed and the context of your code and then comparing them with pre-known information that it has been fed already, with this level of knowledge it can then provide intelligent suggestions in real-time.

Is Codeium safe?The simple answer is Yes it is safe, the explanatory answer is that Codeium claims to be SOC 2 Type II compliant, which indicates they have security controls in place to protect user data.

How is Codeium freeThe straightforward answer is, as an individual it is free, but as a team, you will have to patronize with your money. The truth is there is nothing of value that is really free, there will always be a catch, and in this case that catch is, as a team, there is so much you will want to do with software like this, one of its very importance will only be utilized as an organization. This takes us to our next section talking about it’s pricing.

Codeium PricingCodeium has three prices which are the Individual: Free forever, Teams which starts from $12/month and there is Enterprise plan, but the pricing is not available on the website. You can contact Codeium for more information.

Here’s a summary of the features included in each plan:

Individual

  • Rapid code autocomplete
  • In-editor AI chat assistant
  • Repo natural language search
  • Unlimited usage
  • Trained on open-source data
  • Support via Discord.
  • End-to-end data encryption.

Teams (all features of Individual)

  • Admin usage dashboard
  • Seat management and invites
  • Upto 200 seats
  • Advanced personalization on your codebase.
  • Doc searching (coming soon)

Enterprise (all features of Teams)

  • Priority support via Slack channel with Codeium team
  • Enterprise support portal with documentation and ticketing
  • Saas, on-prem air-gapped, and self-hosted VPC deployments available (optional)
  • Optional finetuning on your codebase.

ConclusionCodeium offers a promising alternative to Git Copilot, providing developers with an efficient tool for improving coding productivity. While there are other options available, Codeium stands out for its integration with Visual Studio Code and its commitment to data security, Thank you for reading this far, your time was highly appreciated. Please take a look at the Docs as it has extensive use-cases for every IDE.

The post Codeium: The Best Github Copilot Alternative appeared first on Semaphore.

View Details

Have you ever noticed that some websites load really quickly while others take a long time? Caching is a key part of how fast a website loads, and Next.js has powerful built-in features to help you use it well. You can make your app run faster and give users a better experience by optimizing the cache, even if you’re not launching on Vercel while potentially saving on hosting costs. Caching basically saves data that is used a lot so that it can be quickly retrieved. The server can serve a cached version of a page from your Next.js app when a user wants it instead of having to render it from scratch. This translates to:

  • Lightning-fast load times: Pages that have been cached load almost instantly, giving your guests a smooth and responsive experience.
  • Less work for the server: When you serve stored pages, your server has more resources to do other things, which makes your application more scalable

To put it simply, optimizing the cache keeps your Next.js app going at its fastest, which makes users happier and lowers server costs.

Next.js gives you a number of ways to improve your cache in a setting that doesn’t depend on Vercel. It automatically caches statically generated pages, improving performance for frequently accessed parts of your application. You can also control data fetching cache behavior by specifying the duration the browser stores the data, ensuring it stays fresh while optimizing speed. For advanced scenarios, you can build custom caching logic using libraries or techniques like local storage. By utilizing Next.js’ built-in caching mechanisms and data fetching strategies, you can greatly enhance your application’s performance without depending on an external platform like Vercel.

Static Site Generation (SSG)The practice of assembling and displaying web pages at build time as opposed to on-demand is known as static site generating. With HTML, JavaScript, and CSS among other static files, this produces a set that is prepared for user delivery. SSG is accomplished in Next.js via use of functions like getStaticProps and getStaticPaths. Run during build time, these routines retrieve data and produce the HTML required for every page. You can retrieve data and send it as props to your page with getStaticProps, so the content is ready even before the user requests it.

Benefits of Build Time Caching of Static HTMLPre-rendering of your information gives SSG a number of benefits:

  • Lightning-Quick Load Times: Your visitors will have a seamless experience as pages load nearly instantaneously because the HTML is already built.
  • Greater Scalability: Because SSG doesn’t have to make content for each request, it lightens the strain on your server. This makes traffic handling by your application more efficient.
  • Improved Search Engine Optimization: Pre-rendered content allows search engines to crawl and index your website faster. SSG may result in lower server costs because of a lesser server load.

Implementation Tips and Best Practices for SSG in Next.jsNow that you understand the core SSG process, here are some key implementation tips to optimize your workflow:

  • Choose the Right Content: Periodically updated material, such product pages or blog posts, is best served by SSG. It works less well with often changing material.
  • Use getStaticProps: This Next.js function is your workhorse during the construction process. Use it to retrieve data and make it available to your page components. Access the source code via github

export async function getStaticProps(context) { // Fetch data here const res = await fetch('https://api.example.com/data'); const data = await res.json(); // Return as props return { props: { data }, };} * Dynamic Routes: For dynamic pages, use getStaticPaths and indicate which routes need to be pre-rendered:

export async function getStaticPaths() { const response = await fetch('https://api.example.com/products'); const products = await response.json(); // Set fallback: false to indicate all pre-defined paths are generated at build time return { paths: paths, // Assuming 'paths' is defined elsewhere with product IDs or slugs fallback: false, }; } Incremental Static Regeneration (ISR)ISR builds on the foundation of SSG. During the build process, Next.js renders your application’s HTML pages with the most recent data. These pre-built pages are subsequently served to users, providing extraordinary speed. While ISR uses pre-rendered pages in the same way that SSG does, it adds dynamism. When a request for a page is received, Next.js examines the cache. If the cached page is fresh (within a defined time frame), it is served immediately. If the cached material is obsolete, Next.js initiates a background re-generation process to retrieve new data and update the HTML content. The user continues to see the cached page while the update occurs in the background, ensuring a smooth experience.

It’s important to understand that ISR doesn’t achieve real-time updates on static pages at runtime. Why? This is because while the page is being regenerated in the background, the user continues to see the cached version. This re-generation can take a few seconds, depending on how you retrieve data. The revalidation window you set determines how fresh the item is. By default, there is no revalidation, but you can choose a time period (for example, 60 seconds) for Next.js to check for updates. This means that the content may be slightly out of date compared to a really real-time system.

How to implement ISR in Next.js applicationsNext.js makes implementing ISR straightforward. Here’s a basic example:

export async function getStaticProps() { const response = await fetch('https://api.example.com/data'); const data = await response.json(); // Revalidate the data every 60 seconds (can be adjusted) return { props: { data: data, }, revalidate: 60, // In seconds };} In this example, getStaticProps retrieves and returns data as props. The revalidate parameter controls how frequently Next.js checks for updates in the background. When new data becomes available, the cached page is automatically refreshed. Incremental Static Regeneration allows you to build Next.js applications that are both fast and keep your content fresh. Understanding ISR’s strengths and limits allows you to properly use it to produce a seamless and up-to-date user experience.

Cache-Control HeadersCache-Control headers are necessary for managing the caching behavior of online resources. They teach browsers and intermediary caches such as CDNs how to handle caching, which can have a substantial impact on a website’s performance and efficiency. They determine how, where, and how long a resource should be cached. They help to reduce bandwidth utilization and server burden, resulting in faster load times. They ensure that consumers receive the most current material without excessive server requests.

Setting Up Cache-Control Headers in Next.jsWhile Next.js automatically configures some default Cache-Control headers, you may want to change them for specific cases. Here is a summary of the options:

  • During SSG, you can define Cache-Control headers within the getStaticProps function. This approach is ideal for static content with a set expiration time.

export async function getStaticProps() { // ... fetch data return { props: { data: data, }, revalidate: 60, // Optional: revalidate every 60 seconds with ISR cacheControl: 'public, max-age=3600', // Cache for 1 hour };} * For API routes, you can set headers directly in the response object.

export default async function handler(req, res) { const data = await fetch('https://api.example.com/data'); res.status(200).json(data).setHeader('Cache-Control', 'public, max-age=86400'); // Cache for 1 day } Best Practices for Cache-Control Headers* Differentiate between static content (e.g., blog articles) and dynamic content (e.g., user profiles) and use appropriate headers. Static material can have longer cache periods, whereas dynamic content may need shorter caching or none at all. * Set appropriate max-age values based on your content’s update frequency as it specifies how long a resource can be cached before being considered stale * Use the immutable directive for really static items that never change (for example, photos or JavaScript files). This informs caches to never re-validate the resource, optimizing performance.

export async function getStaticProps() { // ... fetch data (for non-immutable content) return { props: { data: data, }, cacheControl: 'public, immutable', // Never revalidate }; } * Use no-cache and no-store together with appropriate revalidation mechanisms (for example, SSR or ISR) to ensure that users always view the most recent version. However, use caution because they can have a considerable impact on performance if used excessively.

export async function getServerSideProps() { // ... fetch data (for dynamic content) return { props: { data: data, }, cacheControl: { // Revalidate every 60 seconds with ISR (consider appropriate strategy) maxAge: 60, noCache: true, // Invalidate cached data but doesn't prevent caching entirely noStore: true, // Don't store data in any cache (use with caution) }, }; } * Try out several Cache-Control header settings and monitor real-world performance from browser developer tools in the network and timing tabs or tools like Google PageSpeed Insights, Lighthouse and so on to determine the best mix between the benefits of caching and the freshness of the content.

Client-Side CachingClient-side caching includes storing data obtained from the server on the user’s browser. This cached data can then be utilized for future requests, avoiding the need to retrieve it from the server. Typical client-side caching methods include:

  • Browser Cache (HTTP Cache): Using Cache-Control headers from the server, the browser stores resources in cache on its own. Some of these headers are already handled by Next.js by default.
  • Scripts known as “service workers” operate in the background of a webpage even when it isn’t being used actively. If there is cached data available, they can serve it and intercept network queries.
  • Web Storage: There are two primary choices for web storage offered by the browser:
    • localStorage: Information is kept around even when the browser window is closed.
    • sessionStorage: When a browser window or tab is closed, data is removed.

Implementing Client-Side Caching in Next.jsHere are two approaches to implement client-side caching in your Next.js application:

  1. Service Workers for Granular Caching: Service workers provide you more control over how you cache. To add service worker to your Next.js file, we must first register service worker in a global scope environment and create a service worker file that contains all of our service worker code. You can access the code through GitHubFirst, let’s register a service worker in _app.js by adding the following in the function:

useEffect(() => { if ('serviceWorker' in navigator) { navigator.serviceWorker .register('/service-worker.js') .then((registration) => console.log('scope is: ', registration.scope)); } }, []); We are registering the service worker in _app.js to enable application-wide caching. This ensures that the service worker has access to all resources (pages and assets) in your Next.js project. This allows it to possibly cache data and requests for whatever page the visitor visits. Head over to the public directory and create a new file called service-worker.js. This file serves as the control center for our client-side caching implementation. Paste the following in the file you created

const installEvent = () => { self.addEventListener('install', () => { console.log('service worker installed'); });};installEvent();const activateEvent = () => { self.addEventListener('activate', () => { console.log('service worker activated'); });};activateEvent(); After saving service-worker.js, reload the browser page with “Offline” unchecked in DevTools. Enable “Update on reload” to ensure the service worker updates with each page refresh. The status below shows that the service worker is activated and running.

To accomplish efficient client-side caching for the current user’s page, we use the clone() method from the service worker’s fetch event handler. This allows us to cache the entire page’s assets at once. Add the following to your service-worker.jsfile

const cacheName = 'v1';const cacheClone = async (e) => { const res = await fetch(e.request); const resClone = res.clone(); const cache = await caches.open(cacheName); await cache.put(e.request, resClone); return res;};const fetchEvent = () => { self.addEventListener('fetch', (e) => { e.respondWith( cacheClone(e) .catch(() => caches.match(e.request)) .then((res) => res) ); });};fetchEvent(); We will have the following in your cache and you can now select the “offline option” and reload the page.

  1. Local Storage for Smaller Cached Data in Next.js: You may store key-value pairs of data straight inside the user’s browser using the web storage API localStorage. In contrast to session storage, which disappears when the browser window or tab is closed, this data remains when the browser window is closed. It saves user preferences like as language settings, themes, and accessibility options to ensure a tailored experience across sessions. Temporarily save partially completed forms to avoid users from losing information if they mistakenly navigate away. To keep user login state across page refreshes, it securely store authentication tokens (encrypted properly).Here’s a basic example demonstrating how to use localStorage to cache data in a Next.js component. We will create a custom hook to access the local storage with the following code. You can access the code via Github.

import { useState } from "react";const useLocalStorage = (key, initialValue) => { const [state, setState] = useState(() => { // Initialize the state try { const value = window.localStorage.getItem(key); // Check if the local storage already has any values, // otherwise initialize it with the passed initialValue return value ? JSON.parse(value) : initialValue; } catch (error) { console.log(error); } }); const setValue = (value) => { try { // If the passed value is a callback function, // then call it with the existing state. const valueToStore = value instanceof Function ? value(state) : value; window.localStorage.setItem(key, JSON.stringify(valueToStore)); setState(value); } catch (error) { console.log(error); } }; return [state, setValue];};export default useLocalStorage; In index.js, you can use it as follows:

import useLocalStorage from "@/hooks/useLocalStorage";import { useState } from "react";export default function Home() { // Get the value from local storage if it exists const [value, setValue] = useLocalStorage("favoriteNumber", ""); // Set the favorite number state using the value from local storage (or the empty string) const [favoriteNumber, setFavoriteNumber] = useState(value); // When user submits the form, save the favorite number to the local storage const saveToLocalStorage = (e) => { e.preventDefault(); setValue(favoriteNumber); }; return ( <div> <label htmlFor="number">Your favorite number</label> <form onSubmit={saveToLocalStorage}> <input id="number" value={favoriteNumber} onChange={(e) => setFavoriteNumber(e.target.value)} /> <input type="submit" value="Save" /> </form> </div> );} CDN IntegrationPicture a network of servers positioned thoughtfully throughout the globe. Such is a Content Delivery Network (CDN). By integrating a CDN with your Next.js application, you essentially spread your static content—images, JavaScript, and CSS—over these servers. By bringing your material closer to users, loading times are greatly accelerated.

Benefits of CDN Integration Faster Load Times: By serving material from the closest CDN server, users experience is delivered with less latency. * Greater Scalability: High traffic spikes can be managed by CDNs without affecting your primary server. * Reduced Bandwidth Costs: Your server will be less taxed when static material is offloaded to the CDN, which may result in lower bandwidth bills. * Increased Availability:* Because CDNs are dispersed geographically, your material is still available even in the event that one server goes down.

How to Integrate a CDN in Next.js ApplicationsNext.js provides a simple method for CDN connection. This is the broad outline:

  1. Select and configure your CDN provider. Examples of CDN providers are Cloudfare, Fastly all of which offer significant benefit over Vercel. You’ll be given a CDN URL to serve your assets from.
  2. Within your next.config.js file, specify the assetPrefix. Next.js is instructed to add this URL to the paths of your static assets. An illustration setup is this:

module.exports = { // ... other configurations assetPrefix: 'https://your-cdn-domain.com/',}; 3. Replace ‘https://your-cdn-domain.com‘ with the URL provided by your CDN and deploy your application to a platform that supports CDN integration. You can get more helpful details here.

ConclusionCache optimization in Next.js is a complex technique that greatly improves cost effectiveness as well as performance. Pre-rendering pages at build time with Static Site Generation (SSG) allows developers to guarantee quick delivery and less server load. This method is improved even more by Incremental Static Regeneration (ISR), which permits real-time page updates without compromising the advantages of static generation.

Cache-Control headers must be used strategically in order to control content storage and delivery and to allow developers to customize caching behavior to suit the requirements of their applications. Using Service Workers and localStorage to cache browsers on the client side can dramatically shorten load times and enhance user experience.

Ultimately, connecting with Content Delivery Networks (CDNs) broadens the use of cache optimization by dispersing content over a worldwide network to guarantee quicker access from any area. Next.js’s all-encompassing caching method not only speeds up content delivery but also makes best use of available resources, making it a necessary technique for contemporary web development.

The post Cache Optimization on NextJS Without Vercel appeared first on Semaphore.

View Details

Testing remains a critical yet often misunderstood and undervalued component of software development. What’s more, as artificial intelligence threatens to reshape the role of testers, questions of job security and demonstrating value become increasingly pressing. From the struggle for recognition and early involvement in the development process to the delicate balance between automation and human insight, testers navigate a minefield of technical and organizational hurdles. Michael Larsen, a veteran with over three decades of experience in the field, sheds light on the complex challenges facing the software testing industry today.

Edited transcriptionMichael Larsen’s experience using computers was mostly limited to music production when he started working at Cisco in the early 1990s as an engineer. Soon enough, his aptitude and interest moved him to work as a tester.

After a decade at Cisco, Michael sought to broaden his horizons and transitioned to independent consulting, a path he continues to embrace today, even with the realities of an unstable job market. “After a 30-year career,” Michael jokes, “I tell people that I am an independent software testing consultant because it sounds better than saying that I’m basically unemployed, which is the truth.”

Software Testing Strategies: Skills and ethics for testersTo share his vast experience in the software industry and testing practice, Michael recently co-authored with Matthew Heuser Software Testing Strategies. Published by Packt Publishing, the book delves into various aspects of testing, making it a valuable tool for testers of all levels.

Software Testing Strategies consists of 16 chapters divided into three parts. The first part consists of testing knowledge and skills for testers; “things you should know as a tester,” says Michael. While experienced testers might find this section familiar, it includes a valuable chapter on specialized testing in in-demand areas like modern web applications, SaaS, microservices, and APIs. Michael clarifies that the book does not attempt to turn readers into experts on these technologies. Instead, it provides a solid base for understanding how testing applies to them: “You fired up a tool, you know how to use it superficially. That’s great. There’s a whole lot more you need to learn before you can say that you’re proficient at it.” Hence, he encourages exploration and further learning beyond the book’s introductory guidance.

The book’s second part dives into software delivery processes and the testing strategies that work best within those frameworks. Michael stresses the importance of flexibility and adaptation, rejecting a “one-size-fits-all” approach to testing. He uses a recipe analogy to illustrate this point. Testing, like a recipe, has core principles that remain constant. A deep understanding of these core principles allows testers to improvise and test software in various contexts, coming back to them when needed.

Besides, once you have a grip on how this recipe works, you can tweak it to match your requirements: “You can change the flavor profile a little bit or the development environment has changed and what used to work with the team that you were with before may not work here,” Michael affirms. A deep knowledge of what underlines the “recipes” makes it possible to adjust it to suit your space problem. Michael believes it is necessary to understand the core principles of testing (“recipes”) to adapt and improvise in different environments.

The final section of Michael’s book ventures into the often-overlooked territory of the “political landscape” of software testing. Here, he tackles the undervaluing of testers, the ethical dilemmas they face, and strategies for navigating challenging situations. This last section, Michael admits, has garnered the most interesting feedback, as it addresses crucial issues rarely explored: “Let’s talk about the difficult people you’re going to have to deal with, the difficult conversations you’re going to have, the fact that you may be asked to compromise your ethical standards or somebody might term something in a way where you’re doing what you think is the right thing and somebody else is misrepresenting your work and you’re getting thrown under the bus when something goes wrong.”

Empowering testers for quality, usability, and inclusivityFrom his experience in the industry, Michael doesn’t hide that testers can be “very highly valued or can be denigrated, treated like a second class citizen.” Yet Michael defends the tester role as irreplaceable for achieving quality, provided certain conditions are met. “If you’re not willing to give the testers autonomy and they don’t get the chance to make the decisions or they don’t get a chance to be part of early conversations, then no, we can’t really assure quality; we can’t even really provide you with good testing,” says Michael.

Quality hinges on empowering testers with autonomy and authority over decisions, and also, on their involvement in early project discussions. According to Michael, testers are most valuable at the beginning of the development process, “before line one of a feature is even written.” Their role is to ask critical testing questions from the outset, acting as a voice of caution that can ultimately prevent future problems. Testers should be involved in defining how features will be tested, including considerations for APIs and potential user scenarios, and proactively examining “what if” questions, which significantly contribute to a well-rounded product.

The involvement of testers in development isn’t limited to identifying potential problems but also to anticipating a wider range of user experiences. In this regard, Michael challenges the common perception of accessibility features being solely for people with disabilities. While accessibility features like screen readers or closed captions are typically seen as aids for those with visual or hearing impairments, “everybody can benefit from accessibility enhancement because every one of us can find ourselves in a disadvantageous situation and we don’t have to be disabled to be in it,” Michael points out. Even a well-designed, accessible product can become unusable due to external factors. For instance, bright sunlight can make it difficult to see a screen, or a noisy environment can render audio features useless. In these scenarios, accessibility features act as a safety net for every user, ensuring usability even in less-than-ideal conditions.

However, Michael also cautions that a highly compliant product can still fail if it’s simply unpleasant to use: “You can have the most compliant software product…but if it’s terrible to use, if people don’t like interacting with it, then you’ve won the battles but you’ve lost the war.

Is test automation overhyped?Technology hype cycles end up in two ways, they either don’t stick around or “they work with the way you do things.” In the case of test automation, by itself it has been around for a long time, still, Michael argues it has limitations: ” Automation can do a lot of neat things. It can’t do everything.”

Automation is great for repetitive tasks and following established paths, as it ensures reliable and consistent testing of programmed functionalities. However, once an automated test is written and integrated, the “interesting work” of discovery is mostly done, as automation is incapable of uncovering new issues. Test failures often indicate code changes or library updates, not necessarily new product issues.

Fixing these failures becomes change management, not an active exploration of the product. Hence, Michael understands that at this point automated tests become maintenance tools that check if things work the same after code changes. Besides, in this stage automated tests are also subject to diminishing returns. All they can do is manage known test cases, however, automating every possible test case might not be necessary or efficient. Sooner or later will come a time where the time and effort invested in automation doesn’t yield significant benefits.

In this way, Michael believes it is necessary to implement automated testing specifically where it is more critical. For example to prioritize critical tests that ensure core functionalities work as expected. What’s more, Michael believes these critical tests should be integrated into the development pipeline to catch regressions early. Staging environments can’t perfectly replicate real-world conditions. Production testing helps identify user behavior and the adoption of new features. Besides, users and unexpected situations can reveal issues missed in controlled environments.

The place of AI in testing“If a technology can come along that can do a lot of what they feel you can do and save a lot of money, any logical organization is going to do that,” says Michael on the rise of AI in test case generation. Still, he sees AI as freeing testers from repetitive and manual labor and forward to more challenging issues. Being responsible for more complex issues can be intimidating, however, there’s no alternative: “I’m going to have to think about what value I bring now that say chat GPT or copilot can’t do exactly what I’m doing. If the answer is nothing, you’re in a dangerous position,” says Michael.

Since testers need to showcase their unique value proposition beyond what AI can do, another way to look at complexity in testing, and that can make testers value their work properly in the face of becoming obsolete, is seeing testing as risk management. Regarded as such, testing is not about “going through the motions and hammering on the product”, but, as Michael puts it, about “understanding what are the risks that we face.”

The bottom lineYou can follow Michael on Linkedin. To learn more about his work, you can read his testing blog TESTHEAD and listen to his podcast, The Testing Show.

Grab a copy of his co-authored book, Software Testing Strategies, published by Packt Publishing. Discussion groups about the book are also available on Facebook and LinkedIn.

If you are a hard rock fan, check out Michael Larsen’s band, Ensign Red, on Spotify, Apple Music, and Amazon for a taste of his musical side.

The post Michael Larsen on Quality Assurance in The Age of AI appeared first on Semaphore.

View Details

Bootstrapping a tech startup by yourself or with a small team with no money is no easy feat, but it is not impossible. In this article, I’ll highlight how you can save your cash resources when trying to start your startup and the right mindset you should have.

Being a tech entrepreneur is one of the most exciting and rewarding ventures you can embark on, whether you’re a developer or someone interested in starting a technology business. It’s fascinating and beautiful to see something you started come to fruition as people pay to use your product. But how do you plan a startup when you don’t have any money?

Is the problem real? Will target customers pay for your solution?Is the problem you’re trying to solve real? This question is shockingly important, as many founders create products that don’t solve any real issues, or solve problems that people are not willing to pay to resolve. I recently conducted a survey for a product idea I had, and I was surprised to find that while the participants acknowledged the problem my product aimed to solve, they weren’t willing to pay for the solution.

The leading cause for startup fail is lack of market need. In other words, about 42% of startups fail because they find no customers willing to pay for the proposed solution.

For your startup to make any profit at all, you need to build something that people are willing to spend their money on, if not it will likely fail.

Team and PlanningWhen establishing a team for a startup that does not involve substantial money, the most important issues are strategic planning and resource management. It is wise to have a small and flexible team with varied skills that are important in the first steps of the startup. Many times, this involves having co-founders or first employees who can take on different hats, such as designing and marketing the products, while at the same time handling operations activities.

If you’re not a technical founder, and you don’t want to get bugged down by taxes and organization structure, you can find yourself a technical cofounder at techcofounder. If its someone you’re acquainted with, that’s even better, as it makes communication and decisions much easier.

Since you’re trying to get up and running quickly, it’s best if you get a cofounder that is really technical and know what they are doing and wouldn’t need to get another set of helping hands since they are able to wear many hats in the organization. Another way you can go about this if you have the time is to learn how to code. This route is time-consuming, and it’ll be smart to have another source of income to sustain yourself while you learn to code and build your startup by the side.

On the other hand If you’re a technical founder seeking a non-technical co-founder, the challenge lies in effectively selling your vision. Successfully doing so can significantly reduce startup costs and the time needed to develop your MVP.

Let’s get into fun part of how you can spend little to no money by leveraging some free tools when building your startup.

How to spend little to no money?Many people give up before they even try to build something because of the mindset they’ve created, thinking they need a lot of money to start. Their grand idea is almost immediately dismissed due to concerns about the resources they’ll need to make their dream a reality. In this section, we’ll discuss how to start with almost no money. We’ll cover starting lean and later touch on open-source tools that you can leverage for free in your startup journey.

Build an MVPSpeed is very important when trying to ship your product. Because of this, you’ll need to prioritize building a minimum viable product for your business. A minimum viable product should contain the main features that solve the problem your target customers have. Any other feature that does not solve that problem should be on the sideline. In the words of Eric Ries who wrote The Lean Startup

The lesson of the MVP is that any additional work beyond what was required to start earning is waste, no matter how important it might have seemed at the time.

As a founder it’s easier to build out your MVP if the you have personally faced the problems you are trying to solve. This experience with the problem makes you really experienced in tackling the problem by drafting a quick solution through your MVP.

Adopt an iterative development process, launching with a basic yet functional version of your product, and refine it based on feedback from initial users. This method not only saves upfront costs but also ensures that further investments are guided by actual user demand and feedback, maximizing every dollar spent.

Leverage free and open-source software and toolsWhen building your startup, try your best not to drop a ton of money on expensive tools and services just to get your product live and working, instead utilize free and open-source software tools and services for development, design, and project management to cut costs significantly.

Let’s look at some of these tools below:

  1. Developing Your Product: Use open-source software like Git for version control and GitHub for repository hosting. Use frameworks like React, Vue.js, or Angular for front-end development and Node.js, Django, or Ruby on Rails for back-end development.
  2. Designing on a Dime: Use Krita for image editing and digital painting. For designing sleek vector art or layouts, try Inkscape. Additionally, if you’re looking for a collaborative UI/UX design tool for your team, a good choice would be Figma.
  3. Setting Up Shop Online: Build your website with WordPress. It’s not just for blogs—you can create full-fledged business sites with it, and there’s a huge community for support.
  4. Staying Organized: Keep your projects and tasks in line with tools like Trello or Asana. They’re great for mapping out what needs to get done and tracking progress. Notion is another excellent tool for project management and documentation. Basecamp offers an affordable plan, and Operately, which is being developed by the founders of Semaphore, is a promising open-source option.
  5. Communication is Key: Don’t let communication costs creep up. Use Discord or Slack for messaging; both are perfect for small teams.

Marketing and scaling at no costIn your company’s early days, you need to get creative and cut costs by leaning into low-cost marketing ideas for startups. Think PR, partnerships, content marketing, and crowdfunding to get your product out there without spending a ton. These strategies are key to ramping up your growth and boosting your revenue without the hefty price tag.

Here are some tips to save cost while trying to market and scale your startup:

  • Leverage Social Media: Utilize platforms like Instagram, Twitter, and LinkedIn to share engaging content about your startup. Regular posts, engaging with other users, and using relevant hashtags can help spread the word without any cost.
  • Content Marketing: Start a blog or YouTube channel related to your industry. Providing valuable content can attract an audience, build your brand, and establish authority in your field.
  • Networking: Attend free industry meetups, webinars, and other events to network with potential customers and partners. Building relationships can lead to opportunities and exposure.
  • Partnerships: Collaborate with other businesses to offer joint promotions or barter services. This can open up your products to new audiences without incurring direct marketing costs.
  • Customer Referrals: Encourage satisfied customers to refer others. Offering incentives can boost this effort, which can be a powerful tool for organic growth.

Having the right mindsetBefore you start building on your startup, it’s important to have the right mindset. Resilience and a strong problem-solving attitude—as these often determine your success. Many founders falter due to a lack of resilience in following through with their plans. A resilient mindset acts as an antidote to skepticism, powering you through your startup journey.

While building your startup, you may face numerous challenges including failures, setbacks, and rejections. Having a resilient mindset during this period can be crucial to whether your startup fails or succeeds. You also need strong problem-solving skills that focus on actual problems needing resolution. Keep your focus on how your product or service can benefit your customers.

I’ll end the article with this quote by Nelson Mandela

“Do not judge me by my successes, judge me by how many times I fell down and got back up again.”

ConclusionIn this blog post you’ve learned and gotten insight on how to build your startup with little to no money and the right mindset to have when building a tech startup. Hopefully, the knowledge gained from this tutorial would be able to guide you in your journey as a successful entrepreneur.

The post Zero Budget Big Dreams: Starting a Tech Startup from Scratch appeared first on Semaphore.

View Details

Large Language Models (LLMs) are becoming more and more employed in various applications, ranging from natural language processing to predictive analytics.

Considering the resources needed to use LLMs, in this article we’ll discuss how to keep down the costs associated when self-hosting LLMs. In the case of self-hosting, LLM costs, in fact, can be prohibitive, especially for mid-sized companies. So, in this article, we provide practical strategies to help you manage and reduce the costs associated with self-hosting LLMs.

Strategy 1 to Keep Self-hosted LLM Costs Down: Optimize Model SelectionThe first step to keep LLM costs down is selecting the right model for your needs. Not all applications, in fact, require the largest, most powerful models available. So, here are some considerations to keep in mind:

  • Performance requirements. Determine the level of performance you need. For example, if your application involves basic text classification or sentiment analysis, smaller models like DistilBERT might be sufficient. For more complex tasks, instead, such as generating coherent text or answering detailed questions, larger models like GPT-3 may be necessary.
  • Model size and capabilities. Larger models require more computational resources and this increases the costs. So, evaluate models based on their size, number of parameters, and specific capabilities to ensure you are not overpaying for unnecessary features.
  • Domain-specific models. Consider models that are fine-tuned for your specific domain. These models often provide better performance with fewer resources compared to general-purpose models. For example, if you work in the biomedical field you might find useful the BioBERT model, which is a pre-trained biomedical language representation model based on BERT, designed to handle various biomedical text mining tasks such as named entity recognition, relation extraction, and question answering. Or, if you work in the financial industry, you may find interesting the FinBERT model, which is a pre-trained language model specifically designed for financial sentiment analysis, particularly suitable for tasks such as sentiment classification, financial text summarization, and risk assessment.
  • Open-Source Alternatives: Explore open-source models that can be hosted on-premises. These models often have lower licensing costs and provide more flexibility in terms of deployment and customization. BERT, among the others, is one famous example of open-source LLM.

Strategy 2 to Keep Self-hosted LLM Costs Down: Efficient Resource AllocationConsidering the fact that LLMs use a huge amount of computational resources, proper resource management is essential for reducing the costs of self-hosting these models, considering the cost of the hardware.

Here are some strategies to optimize hardware usage to save costs:

  • Leverage GPUs effectively. To self-host LLMs you need machines that use GPUs, but they are expensive. So, to save costs on GPUs the main idea is to utilize GPU resources effectively by ensuring high utilization rates. This might involve implementing techniques and methodologies like batching requests, optimizing the code to reduce idle times, and using GPU-sharing techniques such as multi-GPU environments.
  • Dynamic resource management. Use virtualization to dynamically allocate resources to the LLM based on demand. Tools like VMwareor Proxmox allow you to create virtual machines (VMs) that can be scaled up or down depending on current needs.
  • Containerization. As containers can be spun up or down dynamically on your self-hosted infrastructure, you can implement a container orchestration strategy with Kubernetes or Docker Swarm to manage resource allocation efficiently. Containers, in fact, allow for better resource allocation and can be easily moved across different environments, ensuring optimal usage of available resources.
  • Implement data parallelism methodologies. Data parallelism involves splitting the data across multiple GPUs or machines, allowing each GPU to process a portion of the data simultaneously. This approach can speed up the training and inference processes, provinding also an efficient use of the available hardware resources. Depending on your hardware setup and the framework you use, to implement data parallelism Pytorch provides the torch.nn.parallel.DistributedDataParallel class while Tensorflow provides the tf.distribute.MirroredStrategy class.

As an example, let’s consider the implementation of data parallelism methodologies with Pytorch.

First of all, you have to discern based on your architecture. The cases are:

  • Single-machine with a multi-GPU setup.
  • Multi-machine setup.

NOTE: if you haven’t already done so, you need to install Pytorch

Single-machine with a multi-GPU setup: In a single-machine setup with multiple GPUs, you can use frameworks like PyTorch (or TensorFlow) to distribute batches of data across GPUs. Each GPU processes its batch independently, and gradients are aggregated and averaged before updating the model parameters.

The implementation could be something like this:

import torchfrom torch.nn.parallel import DataParallel# Initiate the LLM modelmodel = YourLLM-Model()# Parallelize model across GPUsmodel = DataParallel(model)# Move model to GPUsmodel.to('cuda')# Iterate over data loader for data in dataloader: inputs, labels = data inputs, labels = inputs.to('cuda'), labels.to('cuda') outputs = model(inputs) loss = criterion(outputs, labels) loss.backward() optimizer.step() The “magic” with the above code happens thanks to:

  • model = DataParallel(model): the LLM model is wrapped with the method DataParallel(), parallelizes the model across multiple GPUs. This means that the input data will be split and processed in parallel by different GPUs, speeding up the training process.
  • model.to('cuda'): the LLM model is moved to the GPU by calling the .to('cuda') method. This ensures that the model computations are performed on the GPU (which is faster than the CPU for deep learning tasks).
  • The iterations over the dataloader: The dataloader is an iterator that provides batches of input data and labels. In each iteration, it yields a new batch.

Multi-machine setup: For multi-machine setups, use torch.distributed in PyTorch or tf.distribute.MultiWorkerMirroredStrategyin TensorFlow. This approach synchronizes data processing across machines, making efficient use of distributed hardware resources.

For example, the implementation could be something like this:

import torchimport torch.distributed as distfrom torch.nn.parallel import DistributedDataParallel as DDP# Initialize the process group def setup(rank, world_size): dist.init_process_group("nccl", rank=rank, world_size=world_size)# Destroy the process group (to clean up resources used for distributed training)def cleanup(): dist.destroy_process_group()# Train the LLM modeldef train(rank, world_size): setup(rank, world_size) model = YourLLM-Model().to(rank) ddp_model = DDP(model, device_ids=[rank]) optimizer = torch.optim.Adam(ddp_model.parameters()) for data in dataloader: inputs, labels = data inputs, labels = inputs.to(rank), labels.to(rank) outputs = ddp_model(inputs) loss = criterion(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step() cleanup()if __name__ == "__main__": # Define the number of GPUs or nodes world_size = 4 # 4 is just a representation # Handle the parallel execution mp.spawn(train, args=(world_size,), nprocs=world_size, join=True) So, in this case, the code initiates and destroys the processes to save resources and distribute them among different machines. Also, note that, in this case, model = YourLLM-Model().to(rank) moves the model to the appropriate GPU specified by rank, which represents a unique (integer) identifier for each process within a distributed system, that ranges from 0 to “world_size – 1″.

For example, in a machine with 4 GPUs, ranks 0, 1, 2, and 3 would typically map to GPU 0, GPU 1, GPU 2, and GPU 3 respectively.

Strategy 3 to Keep Self-hosted LLM Costs Down: Implementing Model Compression TechniquesAnother way to reduce LLM costs when self-hosting is to use model compression techniques.

Here are some of the most used techniques:

  • Quantization. Quantization is a methodology that reduces the precision of the model’s weights from 32-bit floating-point to 16-bit or even 8-bit integers. This decreases the model size and speeds up inference times, leading to lower resource usage. Since, in the case of LLMs, computational models are represented in the memory of GPUs as tensors, to store them we can use different data types like Float64, Float16, or integers. Since the data type you choose impacts the number of digits used in memory, shortening digits passing from floats to integers, for example, reduces memory utilization.
  • Pruning. Pruning is a methodology that involves removing less important weights from the model to reduce the model’s size while maintaining performance within acceptable limits. There are different ways to prune LLMs, most of them still at an experimental stage. The LLM-Pruner is one of them; it tackles the compression of LLMs within the bound of two constraints: being task-agnostic and minimizing the reliance on the original training dataset.
  • Knowledge distillation. This technique involves training a smaller model (called the student) to replicate the behavior of a larger model (called the teacher). In other words, This technique involves using a smaller model that can achieve comparable performance with a fraction of the computational resources. The main idea behind knowledge distillation is to transfer advanced capabilities from leading proprietary LLMs, such as GPT-4, to their open-source counterparts like LLaMA and Mistral.
  • Low-rank factorization. Since LLMs are Neural Networks that contain dense layers performing matrix multiplication, the idea behind low-rank factorization is to decompose the weight matrices of the neural network into products of smaller matrices. This way, we reduce the number of parameters and computations required, leading to faster and more efficient models.
  • Sparse representations. Utilizing sparsity with neural networks can reduce the model size and computation time, especially with LLMs. Typical techniques to use can be L1 regularization and using Sparse Transformers.

As an example, let’s consider how to apply the quantization technique to an LLM using Pythorch.

But first, if you haven’t already done so, you need to install the transformers library:

pip install torch transformers Now, let’s see the quantization example:

import torchfrom transformers import DistilBertModel, DistilBertTokenizer# Load the tokenizer and modeltokenizer = DistilBertTokenizer.from_pretrained('distilbert-base-uncased')model = DistilBertModel.from_pretrained('distilbert-base-uncased')# Tokenize sample input textinput_text = "Quantization can help reduce model size and improve inference speed."inputs = tokenizer(input_text, return_tensors='pt')# Apply dynamic quantization to the modelquantized_model = torch.quantization.quantize_dynamic( model, {torch.nn.Linear}, # Specify the layers to quantize dtype=torch.qint8 # Specify the target data type)# Verify that the model is quantizedprint(quantized_model)# Move the model to the appropriate device (CPU)device = torch.device('cpu')quantized_model.to(device)# Perform inferencewith torch.no_grad(): outputs = quantized_model(**inputs)# Print the output tensorprint(outputs.last_hidden_state) In this example, we applied dynamic quantization to the linear layers of the DistilBERT model with the method torch.quantization.quantize_dynamic(), converting them to 8-bit integers (qint8): this reduces the model size and improves inference speed on supported hardware.

Strategy 4 to Keep Self-hosted LLM Costs Down: Effective Data ManagementEfficient data management can minimize storage and processing costs. This is particularly useful when using LLMs, because, since these models require a huge amount of data, effectively managing them can significantly decrease associated costs.

Here are some best practices to do so:

  • Data preprocessing. To reduce the amount of data an LLM needs to process – thus, saving on hardware resources – a best practice is to preprocess the data before feeding the model with it. In the case of LLMs, you can generally follow these steps for preprocessing data:
    1. Data cleaning. Remove noise like special characters, HTML tags, and irrelevant content.
    2. Handle Missing Data. Address gaps in the dataset through imputation or removal.
    3. Normalization. Standardize text by converting to lowercase, handling contractions, and standardizing punctuation.
    4. Tokenization. Split text into meaningful tokens, like words or subwords.
    5. Stop words removal. Eliminating the common words that add little value to the text (e.g., “the,” “and”).
    6. Data balancing. Ensure diverse and representative data to prevent model bias.
    7. Encoding. Convert text into numerical format using techniques like word embeddings or token IDs.
  • Caching. Caching temporarily stores copies of data to improve access speed and reduce latency. By keeping frequently accessed data in a high-speed storage layer (usually RAM), caching minimizes the need to repeatedly fetch data from slower storage or remote servers. This boosts performance and efficiency, particularly for repeated read operations. So, in the context of LLMs, to reduce the need to repeatedly process or access the same data, you can implement a caching system for frequently accessed or processed data; this will save on computational resources.
  • Data lakes and warehouses. As LLMs need to ingest and process large quantities of data, you should retrieve this data somewhere. Today, data lakes, data warehouses, and data lakehouses (like Snowflake or Databricks, just to name a few) allow you to store and manage large volumes of data efficiently, at low fees. Well, indeed it’s more cost-effective to use them than to store data on your hardware. To get an idea, you can see Snowflake and Databricks pricing models.

ConclusionsIn this article, we’ve discussed four methodologies to keep self-hosted LLM costs down.

The important concept to bear in mind is that these methodologies can be combined together to provide even better results in saving costs.

So, for example, given your needs and infrastructure, you can decide to containerize a pruned LLM, while implementing batching techniques and a caching system.

The post Keeping Self-Hosted LLM Costs Down: Best Practices and Tips appeared first on Semaphore.

View Details

During the development phase of a project, there are some common measures you’d typically take to ensure your code is at industrial standard. These steps include checking if it’s secure or contains not-so-obvious vulnerabilities and ensuring it’s easily compatible with other code.

You can take these steps manually through peer review and other processes. However, this method could lead to more problems or unresolved issues. Automating this procedure will be the best approach for your team, and you can achieve this using static code analysis tools.

Static code analysis tools allow you to analyze your source code without running it. They examine the code for potential issues, vulnerabilities, or violations of coding standards. Some examples of these tools include Coverity, CodeScene, veracode and the focus of this article, SonarQube.

This article lets you understand SonarQube, its features, and why you need it for your project. You’ll get to install SonarQube and have a quick overview of its console. By the end of this article, you’ll gain more confidence in the quality of your code.

Understanding SonarQubeSonarQube is an open-source static code analysis tool. It allows you to continuously monitor, analyze, and improve your code. After your code analysis, SonarQube provides a report with the results.

This report provides detailed metrics to help you understand and improve the quality of your codebase over time. Additionally, it offers recommendations to improve your codebase.

SonarQube is an overall quality management tool. This means it not only does code analysis but also code coverage and a report-generating system for any test you carry out, like unit tests.

Although it’s a Java-based tool, it’s not limited to Java. It can analyze 30+ programming languages and infrastructure as code (IaC) platforms.

SonarQube offers you two key features. The first is the Quality Gate, and the second is the Customizable Ruleset. Quality Gates in SonarQube act as checkpoints in your CI/CD pipeline, ensuring that your code meets predefined quality criteria before it can be merged into the main branch or deployed.

Customizable Rulesets in SonarQube allow you to define specific coding rules and standards tailored to your project’s requirements and guidelines. SonarQube uses these rules during static code analysis to identify issues such as bugs, code smells, and security vulnerabilities.

Why You Need SonarQubeIf you understood the section above, you’ll notice that SonarQube offers you so much. Well, that’s just a bit of what you can do with this tool. The following are some reasons why you should use SonarQube:

Cost savings: No organization loves to waste money. By identifying and fixing issues early in the development cycle, SonarQube can help reduce the cost of software maintenance and support over time.

Easy CI/CD integration: SonarQube integrates seamlessly with CI/CD pipelines, allowing you to automate code analysis as part of your development workflow. This helps catch issues early and ensures that only quality code is merged and deployed.

Confidence in your code quality: If your project needs to adhere to specific coding standards, industry regulations, or security requirements, SonarQube can help enforce these standards, making you more confident about the quality of your codebase.

Security Assurance: With its built-in security hotspots feature, SonarQube helps identify and mitigate security risks in your codebase, protecting your software from potential threats and attacks.

Setting Up SonarQubeThere are several ways to set up a SonarQube server. One is downloading the zip file from the SonarQube website. The other uses the community edition Docker image. In this tutorial, you’ll be using the latter.

PrerequisitesTo install the SonarQube server, ensure you have Docker installed on your machine. Aside from that, you need the following hardware and software requirements:

  • At least 4GB of RAM for small-scale installations and 16GB of RAM for large ones.
  • You’ll need a 64-bit operating system with at least two cores for small-scale installations and eight for large ones.
  • At least 30GB of disk space for small-scale installations
  • You’ll need a web browser to access the SonarQube web interface.

For a complete list of hardware and software requirements, please refer to the SonarQube installation requirements page.

The following steps will guide you in installing the SonarQube server using Docker. This method is more straightforward to set up, and you won’t need to install any additional software like Java.

Step 1: Run the Docker imageOnce you have installed Docker on your machine, you can run the Docker image on your command line.

docker run -d --name sonarqube -e SONAR_ES_BOOTSTRAP_CHECKS_DISABLE=true -p 9000:9000 sonarqube:latest This command pulls and runs the latest version of SonarQube on your machine. You can confirm that it’s running by checking the Docker status.

Figure 1: Docker StatusStep 2: Access the SonarQube web interfaceAfter the installation, it takes a few minutes for the SonarQube server to start. Once it starts, you can access the SonarQube web interface at http://localhost:9000.

Figure 2: SonarQube startup pageStep 3: Configure SonarQubeYou’ll be prompted to set a login and password for the SonarQube server. The default login and password is admin for both.

You’ll be prompted to change the default password once you’ve logged in using the default credentials.

Figure 3: Change SonarQube default settingsInput the new password and confirm it. Once you’re done, you can reaccess the SonarQube web interface.

Figure 4: SonarQube ConsoleQuick Overview of the SonarQube ConsoleTo have a good overview of the console, we’ll set up project and analyze it using SonarQube.

Step 1: Select the project sourceYou can create a new project using the Projects section of the SonarQube console. This section determines where the project source is located. This can be on your local machine or a remote repository like GitHub or GitLab.

In this guide, you’ll create a local project. To do that, go to the Projects section of the SonarQube console and click Create a local project.

Figure 5: Create a new projectStep 2: Add project descriptionAfter selecting the local project, you will be prompted to provide a Project display name, Project key and Main branch name. The project display name and project key** are used to identify the project in the SonarQube console.

The Main branch name is the name of the main branch in the project. The default value is main.

Figure 6: Add project descriptionStep 3: Setup Clean as You CodeClean as You Code (CaYC) is a SonarQube methodology for software development that allows you to take responsibility for the quality of the code you produce, mainly focusing on the new code that you’re adding or modifying.

In this approach, you’ll aim to write clean, readable, and maintainable code from the beginning rather than postponing cleanup or refactoring to a later time.

You’ll be prompted to select your Clean as You Code option. You have the following options:

  • Previous Version: Considers any code that has changed since the project’s most recent version.
  • Number of Days: Specifies a floating new code period based on a specified number of days.
  • Reference Branch: Considers changes made between your branch and a specific reference branch as new code.

For this example, we’ll choose Previous Version.

Figure 7: Configure Clean as You CodeAfter this, click on Create project**, and SonarQube will create your project in the console.

Figure 8: Project createdStep 4: Select Analysis MethodAfter the project is created, you’ll be prompted to select the Analysis method**. The Analysis method lets you specify how you want to analyze your code.

The options are CI/CD tools such as GitHub Actions, Jenkins, SemaphoreCI, etc. For this example, we’ll choose Locally.

Figure 9: Select Analysis MethodStep 5: Set up a project tokenYou’ll be prompted to enter a Project token**. This token is used to authenticate with SonarQube. This token will notify you when you perform an analysis.

Add the project name and click the Generate button to generate the token.

Figure 10: Project tokenAfter which, you can copy this token and click the Continue** button.

Figure 11: Copy tokenStep 6: Run analysis on the projectAfter creating the token, you can now analyze the project. You’ll need to select the format that best fits your build. This can be either Maven, Gradle or for any programming language like JS, Python, Go**, etc.

After which, you’ll need to download the SonarQube Scanner to your local machine – it, in turn, will be running against the SonarQube server we’re running via docker at http://localhost:9000. The SonarQube Scanner is a tool that automatically runs analysis on your code. You can download the SonarQube Scanner from the SonarQube Scanner download page.

You’ll need to configure your project once you’ve downloaded the SonarQube Scanner that fits your OS and architecture.

NOTE: There is no support for the ARM64 architecture. To bypass this, install the JRE and choose the Any option on the installation page.

In the root directory of your project, create a sonar-project.properties file and add the following properties:

```

must be unique in a given SonarQube instancesonar.projectKey=Test-project# --- optional properties ---# defaults to project key#sonar.projectName=My project# defaults to 'not provided'#sonar.projectVersion=1.0 # Path is relative to the sonar-project.properties file. Defaults to.#sonar.sources=. # Encoding of the source code. Default is default system encodingsonar.sourceEncoding=UTF-8

`` Replace thesonar.projectKey` with your project key.

After this, on your SonarQube console, you’ll see a prefilled sonar-scanner command containing your project details. This command should look like this:

sonar-scanner \ -Dsonar.projectKey=Test-project \ -Dsonar.sources=. \ -Dsonar.host.url=http://localhost:9000 \ -Dsonar.token=sqp_448617bdbf2dd550b03fbfffcae92b9011e6f35b Copy the command, navigate to your project directory via the command line and run it.

Figure 12: SonarQube ScannerOnce your execution is successful, as in the image above, switch back to the console. You should now see the analysis results in the SonarQube console.

Figure 13*: SuccessNOTE*: The project used for this tutorial is a local JavaScript project. You can use any project of your choice, but if you prefer, you can clone this one on GitHub. Since it’s a JavaScript project, you’ll need Node Js installed on your machine for the Sonar scanner to work.

Step 7: Analyze the ReportOnce you get back to the console, you’ll see your analysis report. This report is quite detailed and can get confusing, so let’s go through each section.

Figure 14: SonarQube Console Projects SectionThis page contains overall information about the project. On this page, you can see whether your scan succeeded or failed. You can also find the scores of each metric.

These metrics are security, reliability, maintainability, hotspots, coverage and duplications. Scrolling down the page will give you more details, like the size and languages used in the project.

Figure 15: SonarQube Console IssuesIn the issues section, you can see the issues that were found in every file of your project. Under each file, you can find the year the file was created and assign an open issue to a team member.

Figure 16: SonarQube Console RulesThe rules section, as the name implies, contains rules that are used to identify security issues. They are categorized into four types: code smell, bug, vulnerability, and security hotspot.

Each type has specific expectations, such as zero false positives for bugs and code smells and more than 80% true positives for vulnerabilities. Rules can be filtered by language, type, tag, repository, severity, status, etc.

Figure 17: SonarQube Quality ProfilesA quality profile defines the rules and configurations you can apply during code analysis for a specific programming language. It determines the coding standards, best practices, potential bugs, security vulnerabilities, and code smells checked for in the codebase.

You can customize or extend a quality profile to meet your project’s requirements so the quality of your code is consistent across projects.

Figure 18: SonarQube Quality GateA quality gate allows you to define a set of conditions that must be met before a project can be released. They define criteria like bug counts or code coverage.

The default quality gate is “Sonar way”; however, you can create your own. Permissions ensure that only authorized users can edit or manage quality gates. Only users with global administration permissions can modify quality gates by default, but this permission can be delegated to specific experts or groups for individual gates.

Figure 19: SonarQube AdministrationThe administration section allows you to configure your console. Here, you can authenticate with DevOps platforms like GitLab, GitHub, etc.

You can also set general rules concerning your project creation, quality profiles, and the Look & feel of your console.

ConclusionIn this article, you learnt how to analyze a project using SonarQube. In the process, you understood how to use this tool, installed the SonarQube server, and had a quick overview of the console.

All this information might be confusing initially; however, once you master the console, maintaining clean, secure and reusable code will be easy.

Once you’ve gotten the hang of this, you should look out for the next article, in which we’ll go through how to integrate SonarQube into your Semaphore CI pipeline.

The post SonarQube: Open Source Security Automation appeared first on Semaphore.

View Details

In today’s time, many organizations are using cloud technologies. Businesses rely heavily on cloud services for their day-to-day operations. And managing cloud expenses has become more critical than ever. But, with the growing adoption of cloud services, optimizing their costs has become a critical challenge.

According to Gartner’s report of November 2023, global spending on public cloud services is forecast to grow 20.4% to a total of $678.8 billion in 2024, up from $563.6 billion in 2023. As organizations scale up their operations, the cloud costs can go out of control if not managed effectively. Fortunately, many open-source tools are available in today’s time. These tools can help mitigate these costs while ensuring optimal performance and resource use.

Importance of reducing cloud expensesWhen organizations adopt cloud computing solutions they often encounter the task of overseeing and managing cloud expenses. Failure to optimize the use of cloud resources can result in overspending which impacts an organization’s performance significantly. The pay-as-you-go approach in cloud services requires monitoring and optimization to ensure proper resource usage and cost control. Neglecting this model could potentially result in setbacks, for the organization. Let’s explore the benefits that businesses can derive from optimizing their cloud spending:

  • Cost Efficiency: By identifying unnecessary spending, organizations can divide their resources more efficiently.
  • Great Budget Management: Predictable cloud expenses can lead to great budget management.
  • Increased Return-on-investment (ROI): Having good control over cloud resources can lead to an increased ROI.

Now that we understand the benefits & importance, let’s see the 10 open-source tools that will help an organization achieve these cloud cost optimizations.

Overview of open-source tools as cost-saving solutionsThe open-source community has stepped up to offer a mass of powerful and cost-effective solutions. These tools can reveal the path to optimized cloud expenses, as they provide organizations with the necessary insights, automation, and control to navigate the cloud landscape.

Here are the top open-source cloud cost optimization tools, that will help you achieve your goals.

Tool 1: KubernetesKubernetes also known as K8s is a dominant open-source container orchestration platform. It is renowned for its ability to automate the deployment, scaling, and management of containerized applications. It works by simplifying the containerized workloads based on demand, across a cluster of machines to ensure increased availability, scalability, and efficient resource use. We may also call this a container orchestration powerhouse tool.

Cost-saving features of KubernetesThere are numerous features of Kubernetes for cost saving in the cloud:

  1. Autoscaling: Kubernetes consists of some open-source tools (like Horizontal Pod Autoscaler and Cluster Autoscale) that allow the users to dynamically adjust the number of containers to use which prevents the companies from overprovisioning, where they pay for resources that are not being used.
  2. Improved developer efficiency: This also improves the developer efficiency by streamlining deployments, rollbacks, and scaling allowing them to focus on building apps rather than managing cloud infrastructure.
  3. Horizontal scaling: It also gives the users leverage the horizontal scaling, allowing them to distribute workloads across multiple nodes, optimizing resource usage and reducing costs.

Real-World examplesSome companies such as Netflix, Spotify, Pearson, etc. use Kubernetes. These companies used Kubernetes efficiently for their containerized microservices, autoscaling, and other services, and also reduced their cloud infrastructure cost by a large percentage.

Kubernetes GitHubTool 2: TerraformTerraform is better called an infrastructure-as-code tool that helps define your infrastructure resources (like servers, storage, and networks) in declarative configuration files, enabling automation, repeatability, and cost optimization. Terraform has multiple use cases including infrastructure-as-code, managing Kubernetes, managing virtual images, managing network infrastructure, etc.

Note: As of November 2023, Terraform is no longer an open-source project due to the acquisition of HashiCorp by IBM for $6.4 billion, as reported by ycombinator news. If you’re seeking a fully open-source option for the cloud cost optimization, OpenTofu is a promising alternative.

Terraform as a cost-effective infrastructure tool1. Infrastructure Optimization: * Resource Visibility: Terraform provides a clear overview of your entire infrastructure, making it easier to identify and eliminate unused or underutilized resources. * Right-Sizing: You can define resource configurations with the exact specifications your applications require, preventing overprovisioning. 2. Reduced Errors & Faster Configuration: With Terraform, you can automate the whole process, minimizing costly mistakes. Also, the infrastructure changes are applied quickly and consistently through code which saves time and reduces errors. 3. Cloud Cost Management: This tool integration in your application can also provide the early cost estimation of infrastructure based on your configurations. This allows you to make informed decisions about resource types and sizes before provisioning. Not only this, automated checks can also be applied to set it within the budget limits.

Real-World examplesSome companies such as Deutsche Bank and GitHub use Terraform to simplify infrastructure provisioning across multiple cloud providers, leading to greater efficiency and cost savings.

Terraform GitHubTool 3: GrafanaGrafana is an open-source monitoring and observability system that provides powerful visualization capabilities for analyzing and understanding data from various sources, including cloud services

Grafana as Cost Efficiency ToolNow let’s see, how grafana helps in identifying cost inefficiencies and optimizing resource usage.

  1. Visualize Cloud Costs: Integrate Grafana with cloud providers (AWS, Azure, GCP) to ingest cost data. Create custom dashboards that display cost metrics like hourly, daily, or monthly spending by service, project, or region. Utilize heatmaps and other visualizations to identify cost spikes, trends, and anomalies.
  2. Drill Down for Insights: Grafana allows you to drill down into specific cost components, such as CPU, storage, network usage, or database instances. Correlate cost data with other infrastructure metrics (e.g., CPU utilization) to understand the root cause of cost variations. This helps you identify idle resources or inefficient configurations that are driving up costs.
  3. Set Cost Alerts: Configure alerts that trigger when spending exceeds predefined thresholds or when anomalous cost patterns arise. Early detection of potential overspending allows you to take corrective actions, such as scaling down resources or optimizing configurations.

Real-World examplesThe companies that use AWS can actually use the grafana for getting the visualizations by creating their personalized dashboards that will display:

  • Total daily spending on AWS services.
  • Cost breakdown for EC2 instances, S3 storage, and Lambda functions.

While Grafana is a powerful open-source tool, it requires setting up data sources and configuring dashboards to get a detailed cost analysis, effective cost monitoring, and optimization. Here are some of the dashboards available online, to get you started quickly, that specifically focus on cost monitoring:

  1. Amazon Managed Grafana for Cost Anomaly Detection
  2. Kubernetes Dashboard for cost management

Grafana GitHubTool 4: PrometheusPrometheus is another open-source tool that is designed to work as a monitoring and alerting toolkit, especially for cloud-native platforms. It collects and analyzes the metrics from your cloud infrastructure, enabling you to monitor resource utilization and reduce costs.

How Prometheus assists in monitoring cloud resource utilization and identifying cost-saving opportunitiesPrometheus is a powerful real-time monitoring tool that enables alerting capabilities making the one of the reasons to effectively utilize it for cost optimization. There are several features that Prometheus provides including:

  1. Resource Consumption Insights: With the help of this tool, the user can understand how efficiently our resources are being utilized by taking into consideration different metrics like CPU, memory, and storage usage for cloud resources.
  2. Identifying Idle Resources: Another feature of using Prometheus is that we can easily identify resources with consistently low utilization rates, indicating potential overprovisioning.
  3. Cost Correlation: Combine Prometheus metrics with cost data from your cloud provider. This correlation can help you understand which resources are driving up costs and how changes in resource usage affect your cloud bill.

Examples of Prometheus in reducing cloud expensesThere are various examples of using Prometheus to monitor the EC2 instances. With this tool, companies discovered that 20% of instances have consistently low CPU utilization. They right-size these instances to a smaller size, saving on compute costs. SoundCloud (the popular audio streaming platform) is one of the good examples that uses Prometheus for monitoring and cost optimization.

Prometheus GitHubTool 5: Apache KafkaApache Kafka is another open-source tool developed by LinkedIn mainly for distributed data streaming. Apache might not be the most be best fit for cost-saving tools as compared to those discussed above, but it offers several features that can significantly reduce cloud infrastructure expenses specifically when handling large volumes of online data streams. It is being used by more than 80% of all Fortune 100 companies.

Cost-saving features of Kafka, such as efficient message processing and storageApache Kafka has some of its top core capabilities like high throughput, scalable, high availability, etc. But Apache Kafka also offers several features that can actually reduce the cloud infrastructure cost, when handling large amounts of data streams.

There are several features that Apache Kafka provides like:

  1. Efficient message processing: Kafka acts as a buffer between data producers and consumers which is known as decoupling which allows producers to send data at their own pace without affecting consumers and vice versa and finally, reduces the resource consumption on both sides, leading to potential cost savings.
  2. Flexible storage options: Log compaction in Kafka reduces storage costs by removing duplicates and retaining only the latest version, optimizing long-term data retention. Additionally, tiered storage options like Confluent Cloud minimize expenses by offloading older data segments to cheaper cloud object storage.
  3. Resource optimization: Apache Kafka allows the dynamic scale of the clusters in the cloud based on your data volume and processing needs allowing you to prevent overprovisioning and reducing the idle resources cost.

Real-World examplesLinkedIn being the innovator of this tool is the best example of using Apache Kafka to optimize its data processing workflows and reduce cloud expenses.

Apache Kafka GitHubTool 6: ElasticsearchElasticsearch is a distributed search and analytics tool designed to handle large volumes of data efficiently. It is an open-source tool that organizations use to manage and analyze datasets cost-effectively.

How Elasticsearch helps in managing and analyzing large volumes of data cost-effectivelyElastisearch is a free and open-source tool that can help the organization manage and analyze large volumes of data but not all services are free, some need to be paid to use. Reducing the cost with elastisearch can be done by:

  1. Efficient Data Storage: Using the elastisearch’s inverted indexes for optimizing the searches for specific terms reduces the data scanned along with the lower resource consumption. Another tool Elasticsearch ILM (Index Lifecycle Management) automates the process of moving older data to cheaper storage tiers which helps you optimize storage costs for data with varying access needs.
  2. Horizontal Scaling: Elasticsearch clusters can be scaled horizontally by adding more nodes, allowing you to handle growing data volumes without sacrificing performance. This eliminates the need for expensive hardware upgrades.
  3. Search Appliance Alternative: Elasticsearch can replace traditional search appliances, which can be expensive and have limited scalability.
  4. Use data rollups: Summarize data into a single document, then archive or delete the original data.

Examples of Elasticsearch use cases for reducing cloud costsElastisearch is a widely popular and adopted open-source tool that helps many organizations leverage its cost-saving features like data storage, horizontal scaling, etc.

For example, a company that uses Elasticsearch to store and analyze website logs, and if they are implementing ILM, they can easily move older logs to cheaper cold storage, significantly reducing storage costs. Similarly, if a company leverages Elasticsearch to power its e-commerce search engine. The efficient storage and indexing capabilities of Elasticsearch, enable them to handle large product catalogs and high search volumes without incurring high infrastructure expenses.

For those on the AWS cloud platform, AWS OpenSearch Service provides a managed offering with similar functionalities and potential cost-saving benefits.

Elastic GitHubTool 7: HadoopHadoop is a Java-based distributed processing framework that is used for big data analytics and processing, not only this, but its scalable and fault-tolerant architecture enables the consumer to reduce the cost of resources running in the cloud.

Cost-saving benefits of Hadoop for data processing and storage in the cloudOrganizations can benefit from using Hadoop to optimize data workflows, minimize data movement costs, and efficiently process large datasets. As it is open-source, it’s free but that doesn’t mean, all of the services are free. The cost will depend on how much the software is being used and at which scale.

Reducing the cost with Hadoop can be done like:

  1. Scalability: The clusters in Hadoop are easily scalable up or down both ways and its tools allow you to pay only for the resources you use, especially in cloud environments with on-demand pricing models like YARN (Yet Another Resource Negotiator).
  2. Open-Source: Like others, Hadoop also discards the licensing costs associated with proprietary data processing solutions.
  3. Data Lake Architecture: The data lake architecture in Hadoop has the ability to store large volumes of structured, semi-structured, and unstructured data in a centralized data lake, and this can separate data silos and simplifies data management, potentially reducing storage costs.

Examples of Hadoop implementations for cloud cost reductionSeveral companies use Hadoop services like, Adobe, LinkedIn, Facebook, etc. To have a real-life implementation for cloud cost reduction, an organization can use a cloud-based Hadoop cluster to process vast amounts of user activity data and social media sentiment. With this technique, the organization can reduce the on-demand pricing and scale the cluster based on daily/weekly usage patterns, they optimize their cloud spending.

Hadoop GitHubTool 8: OpenStackOpenStack is an open-source cloud computing platform that allows organizations to create and manage their own private clouds. This tool helps organizations optimize cloud expenses through efficient resource utilization, OpenStack becomes a flexible and cost-optimization alternative tool to other paid cloud tools.

How OpenStack enables cost-effective private cloud deployments and managementOpenStack can enable the organization for cost-effective private cloud deployments, the user can optimize some of the below features:

  1. Compute (Nova): OpenStack’s Compute service allows users to create and manage virtual machines, providing an alternative to proprietary compute services like Amazon EC2 or Google Compute Engine. By utilizing Nova, organizations can potentially reduce costs associated with virtual machine provisioning and management.
  2. OpenStack’s Swift: Swift for object storage and Cinder for block storage which offer alternatives to proprietary storage solutions such as Amazon S3 or GCP storage. By using Swift and Cinder, organizations can avoid vendor lock-in and potentially reduce storage costs.
  3. The Distributed Resource Scheduler (DRS): DRS is another feature that distributes workloads across multiple nodes based on resource usage. Balancing workloads optimizes resource usage, reducing costs.
  4. Use Open-Source Tools: OpenStack is an open-source platform. Consequently, you can use open-source tools to monitor and manage your infrastructure. The open-source tools in OpenStack give organizations detailed control over large volumes of resources including computing, storage, etc.

Examples leveraging OpenStack for cloud expense optimizationOpenStack is being used by various big organizations such as IBM, Walmart, VMware, NASA, etc. However, it’s important to acknowledge the trend of organizations adopting a hybrid approach, integrating OpenStack with Kubernetes (k8s). This combination leverages the strengths of both platforms: OpenStack’s infrastructure management capabilities and k8s’ container orchestration expertise. The users can integrate the openstack-integrator to be able to use the OpenStack native features.

Organizations can benefit the cost reduction with this tool with various features like one, by dynamically scaling the OpenStack cluster based on project requirements, optimizing resource utilization, and avoiding paying for idle resources in a public cloud environment. Another best cloud cost optimization can be done by using a distributed resource scheduler (DRS), reserve tools, and more.

OpenStack is very useful for specific use cases where there are predictable workloads, strict security requirements, or fluctuating computational needs, and with this, it provides flexibility, neutrality, and the potential for significant cost savings in the long term for an organization.

OpenStack GitHubTool 9: DockerDocker is an open-source containerization platform consisting of a variety of components that help in container management. Docker can also become a great tool to reduce and optimize cloud expenses, by optimizing resource utilization and streamlining deployments.

But why use Docker to optimize the cloud expenses in today’s time? The answer is simple, docker allows you to package your applications and their dependencies into lightweight, portable containers. These containers share the underlying operating system of the host machine, eliminating the need for full virtual machines (VMs) and thereby monitoring the control of extra pay for services.

How Docker facilitates efficient resource utilization and deployment in the cloudThere are several reasons why docker can facilitate efficient resource utilization and deployment in the cloud and can lead to cost-saving advantages:

  1. Reduced Resource Consumption: Containers require fewer resources compared to VMs. This translates to lower compute, memory, and storage costs in the cloud. By packing more containers onto fewer servers, you can optimize resource utilization and potentially reduce the number of instances needed.
  2. Faster Deployments: Docker containers are self-contained and boot up quickly, accelerating application deployment and scaling processes. This minimizes the time applications spend in a non-productive state, reducing associated cloud costs.
  3. Simplified Management: Docker facilitates consistent deployments across development, testing, and production environments. This reduces the need for separate infrastructure for each stage, potentially saving on cloud resources.
  4. Microservices Architecture: Docker’s containerization model promotes microservices architecture, where applications are broken down into smaller, independent services. This allows for easier scaling and resource allocation based on individual service needs, further optimizing cloud costs.

Examples of Docker for lowering cloud expensesDocker nowadays has become a very popular tool for containerization for many organizations and for some to lower the cloud cost too. Big organizations like Google, AWS, Thoughtworks, etc. are using Docker in their tech stack.

Organizations can leverage Docker to optimize cloud expenses by reducing the overhead associated with VMs and improving deployment efficiency. For example, a company that uses docker containers for its microservices architecture, by scaling individual services based on traffic patterns, can optimize resource utilization and reduce cloud costs compared to running monolithic applications on VMs.

Another alternate tool that organizations can leverage for cost saving is Docker Swarm which is a native container orchestration platform from Docker. It allows for efficient management of containerized applications at scale, load balancing, etc.

Docker GitHubTool 10: Apache SparkApache Spark is an open-source distributed data processing multi-engine that can execute data engineering, data science, and machine learning on single-node machines or clusters. Spark is the best tool for handling the large amount of datasets efficiently. Not only this, it can actually reduce the cloud costs for big data analytics and processing.

Cost-saving features of Spark for big data analytics and processing in the cloudSpark is a powerhouse for distributed data processing where data is processed in parallel across clusters of machines, handling complex data tasks faster as compared to other approaches. But this isn’t its cost-saving feature, it also includes:

  1. Resource optimization: To ensure efficient resource utilization and avoid overprovisioning of compute resources, we can configure resource allocation for different stages of your data processing jobs with the usage of tools like Kubernetes.
  2. Unified Platform: As Spark contains various big data tasks, including batch processing, real-time streaming, and machine learning, we can hide the need for separate tools and associated infrastructure costs.
  3. In-memory processing: Cost can be reduced like storage costs and faster processing times, allowing you to complete tasks quicker and potentially reduce the number of compute instances needed. Spark can leverage in-memory computing for frequently accessed data, significantly reducing disk I/O operations.

Examples of cost reduction using Apache SparkApache Spark is the most widely used engine for scalable computing. It is being used by thousands of organizations, including 80% of the Fortune 500 like Databricks, Yahoo, Netflix, and more.

For example, if an organization uses Spark’s in-memory processing for frequently accessed data, it can achieve faster turnaround times and reduce storage costs compared to traditional disk-based analytics to analyze large datasets of customer transactions for a finance organization. Similarly, if an organization can handle complex computations efficiently and faster, it can lead to cost savings.

Apache Spark GitHubConclusionOptimizing cloud expenses is crucial for businesses striving for long-term financial sustainability. However, it’s a multifaceted challenge. In this article, we explored 10 open-source tools – Kubernetes, Terraform, Grafana, Prometheus, Apache Kafka, Elasticsearch, Hadoop, OpenStack, Docker, and Apache Spark that can reduce cloud costs and may be chosen as cost optimization tools. Additionally, tools like Semaphore CI can be leveraged to streamline and automate cloud deployments, further contributing to cost optimization. For a comprehensive CI/CD solution that streamlines deployments, explore Semaphore CI Cloud. These tools smoothen the operations, minimize wastage, and ensure significant savings in cloud deployments.

The post 10 Open-Source Tools for Optimizing Cloud Expenses appeared first on Semaphore.

View Details

Cloud storage has become the most popular way for businesses and developers to store data for online use. S3 is the de facto standard for cloud storage, with Amazon as the leading provider. However, the monthly bill for Amazon S3 can be steep, often amounting to thousands of dollars per month, mainly due to data transfer (egress) fees. Enter Cloudflare R2, a cost-effective cloud storage alternative that eliminates these greedy egress fees!

In this article, you will understand what Cloudflare R2 is, how it compares to Amazon S3, how its pricing model works, and whether you can migrate your data from S3 to R2.

Let’s dive in!

What Is Cloudflare R2?R2 is a modern, efficient, S3-compatible cloud object service offered by Cloudflare. Compared to typical cloud storage services, such as Amazon S3, it is a more cost-effective solution. That is because Cloudflare R2 allows storing large amounts of unstructured data without costly egress bandwidth fees.

Data egress fees are network fees that cloud providers typically charge for moving data out of the cloud. These, also known as data transfer fees, are usually hard to predict and add up to the fees you already need to pay for storage and cloud computing. Egress fees apply when retrieving files from cloud storage (e.g., to show images on a web page), accessing data from external analytics applications, or transferring data between different regions.

Other scenarios where companies need to move data out of their cloud storage is when implementing hybrid cloud (a mix of two or more cloud environments) or multi-cloud (multiple clouds from more than one cloud provider) architectures. These enable businesses to use the most appropriate cloud environment for each task, improving the efficiency and flexibility of the overall IT infrastructure. In addition, having more than one cloud storage is essential for robust disaster recovery.

By eliminating egress fees, R2 makes it more affordable for businesses and developers to adopt such robust, reliable, and efficient IT architectures. In particular, Cloudflare R2 covers several scenarios and use cases, including:

  • Cloud storage for web applications
  • Storage for multimedia files (e.g., images, podcast episodes, recordings, etc.)
  • Data lakes (e.g., for analytics and Big Data applications)
  • Storage for cloud-native applications
  • Cloud storage for large batch processes (e.g., machine learning model artifacts or datasets)

AWS S3 vs Cloudflare R2Amazon S3, short for Amazon Simple Storage Service, is a scalable object storage service launched by AWS in 2006. Over the years, the S3 standard has become the go-to API for cloud storage. Its popularity has led to the emergence of several S3-compatible providers, but Amazon remains the S3 cloud storage leader.

Cloudflare R2 is a much newer object storage solution, with its first release only in 2021. It offers similar features to Amazon S3, relying on an S3-compatible API. This makes it easy for users to migrate and integrate their existing tools and applications with this cloud system.

The main differences between Amazon S3 and Cloudflare R2 are:

  • Cost-effectiveness: Amazon S3 charges high egress fees for moving data off the AWS network, which can significantly increase overall cloud-associated costs. In contrast, Cloudflare R2 has no egress fees. Users can move data out of storage at no additional cost, resulting in significant savings in data-intensive applications.
  • Free tier: Amazon S3 offers a limited free tier, while Cloudflare R2 comes with a generous free tier that includes millions of requests per month.
  • Global accessibility and performance: Amazon S3 provides solid performance, but only when configured correctly to optimize data transfer between regions. On the other hand, Cloudflare R2 automatically selects the optimal storage region based on the data upload location. This ensures high performance and availability without the need for complex configurations.
  • Cloud storage API: Amazon S3 is based on the original S3 standard, which is widely supported by a wide range of tools and applications. Instead, Cloudflare R2 uses an S3-compatible API, which lacks some S3 features but has others. Full compatibility with the S3 API for all features is currently underway. Monitor the status on the official docs.
  • Data security: The two solutions offer strong security features, including encryption and in-depth access control policies. Cloudflare R2 enhances security even more with undiscoverable bucket names, reducing the risk of unauthorized access.

Cloudflare R2 Pricing BreakdownThe Cloudflare R2 pricing model consists of two following dimensions:

  • Storage: Total volume of data stored in the cloud, per GB.
  • Operations: Operations performed on the data in the cloud. These are divided into Class A and Class B. Class A operations are more expensive and tend to change the state, while Class B operations are cheaper and tend to read the existing state. Some operations, such as deleting an object or a bucket, do not fall into these two categories and are free.

The R2 pricing can be summarized in the table below:

| Rates | | --- | | Storage | $0.015/GB | | Class A operations | $4.50/million requests | | Class B operations | $0.36/million requests | | Data egress | Free |

All included usage is charged on a monthly basis.

Is this actually more cost-effective than Amazon S3 storage? Let’s find it out!

Cloudflare R2 vs Amazon S3: Price ComparisonTake a look at the summary tables below to compare the free and paid plans offered by Cloudflare R2 and Amazon S3:

| Cloudflare R2 | Amazon S3 | | --- | --- | | Data storage | $0.015 per GB | Starts at $0.023 per GB | | Class A operations (POST) | $0.0045 per 1,000 requests | $0.005 per 1,000 requests | | Class B operations (GET) | $0.00036 per 1,000 requests | $0.0004 per 1,000 requests | | Data egress | Free | Starts at $0.09 per GB |

Note that prices are up-to-date at the time of writing but may change over time. Amazon S3 prices and egress fees are based on tiered rates for S3 Standard in the US East region. For more information, visit the Amazon S3 pricing page.

As you can see, Cloudflare R2 is cheaper in all the considered aspects when compared to Amazon S3. Time to see how these price variations translate to a couple of real-world scenarios!

Free Tier LimitsBoth Cloudflare R2 and Amazon S3 come with free tiers, which compare as below:

| Cloudflare R2 | Amazon S3 | | --- | --- | | Data storage | 10 GB per month | 5 GB per month | | Class A operations (POST) | 1M per month | 2k per month | | Class B operations (GET) | 10M per month | 20k per month | | Data transfer | Unlimited | 100 GB | | Expiration | Unlimited | 12 months |

Notice how the biggest difference is the huge amount of free requests that R2 offers. Millions of Class A and Class B operations can meet the cloud needs of a medium-sized web application. Data storage limits are quite restrictive in both scenarios, but R2 offers twice as much monthly storage space as Amazon S3. Plus, Amazon S3’s free tier expires after 12 months, while R2’s has no time limit.

To better understand what you can achieve with a free tier, let’s consider an example. With Cloudflare R2’s free tier, you can develop a site where users upload up to 10,000 1 MB web-optimized images per month. Suppose each web page on the site contains 5 images, this means you could have 2,000,000 monthly visits without incurring any costs. With Amazon S3, you could only have 5,000 uploaded images and 4,000 views a month.

Usage for Video StorageLet’s assume that you are building a video-oriented application. Users will upload 1 million videos per month and view 100 million of these videos in this application. The average size of the video files is 5 MB, which means that users will upload 5 TB of storage per month and download 500 TB of data a month.

To compare the cost of this application on R2 and S3, it is necessary to dig into the Amazon S3 prices. In detail, the S3 pricing model for data storage follows this table:

| S3 Data Storage | Cost | | --- | --- | | First 50 TB/month | $0.023 per GB | | Next 450 TB/month | $0.022 per GB | | Over 500 TB/month | $0.021 per GB |

Similarly, S3 data transfer fees are calculated using the following table:

| S3 Data Transfer Tier | Cost | | --- | --- | | First 10 TB/month | $0.09 per GB | | Next 40 TB/month | $0.085 per GB | | Next 100 TB/month | $0.07 per GB | | Greater than 150 TB/month | $0.05 per GB |

On a monthly basis, this hypothetical application requires:

  • 5 TB of storage
  • 1 million Class A operations
  • 100 million Class B operations
  • 500 TB of data transfer

The Cloudflare R2 vs Amazon S3 price comparison table for this scenario is:

| S3 Data Transfer Tier | Cloudflare R2 | Amazon S3 | | --- | --- | --- | | Storage | $75 | $115 | | Class A operations (POST) | $4.50 | $5 | | Class B operations (GET) | $36 | $40 | | Data egress | — | $28,800 | | Total | $115.5/mo | $28,960/mo |

Sure, Amazon AWS offerings are quite broad and complex, and there must be a solution that better fits this particular case. However, the Cloudflare R2 plan turned out to be 99.99% cheaper than the S3 Standard plan.

Moving From S3 to Cloudflare R2: Costs and How To Do ItYou might now be convinced to switch from your S3 provider to R2, but what about the egress fees to transfer all your data from your cloud provider to Cloudflare? That could take months and cost you several thousand dollars!

Cloudflare is well aware of the data migration problem and has designed two solutions for that:

  • Super Slurper: Move all your data from Amazon S3 or Google Cloud Storage to R2 in a single operation. It preserves custom object metadata from the source bucket by copying it to the migrated objects, and it does not delete any objects from the source bucket. This tool is fast but requires you to pay egress fees from the source provider.
  • Sippy: Incrementally migrate from S3 providers to Cloudflare R2 without paying unnecessary egress fees. When an object is requested, it is served from your R2 bucket if it is found. Otherwise, it is simultaneously returned from the source bucket and copied to R2 to minimize data transfer fees. All other operations, including insert and delete operations, continue to work as usual.

Both are free to use and charge only for the Class A operations that the tool needs to perform to transfer data to your R2 bucket.

Advantages and Limitations of R2 Compared to S3Before blindly moving from Amazon S3 to Cloudflare R2, you must analyze in detail the pros and cons of R2 versus S3.

👍 Pros:

  • Cheaper than Amazon S3 on data storage and operations
  • Much more generous free tier
  • No egress/data transfer fees
  • S3-compatible API
  • Easier setup, with automatic region configuration
  • Undiscoverable bucket names
  • Simple data migration with Super Slurper and Sippy

👎 Cons:

  • Not all S3 API operations are currently supported
  • Super Slurper only works with Amazon S3 and GCS, can only transfer objects less than 1 TB, and does not guarantee the same ETags
  • More limited ecosystem compared to AWS

ConclusionIn this article, you explored Cloudflare R2 and compared it to Amazon S3. You now know that R2 is a cost-effective cloud storage solution with an S3-compatible API. Thanks to its no-egress fees policy, R2 can save you thousands of dollars a month. However, migrating from Cloudflare R2 to Amazon S3 would mean losing the ecosystem advantages of keeping your data within the AWS infrastructure.

The post Cut Down S3 Storage Costs with Cloudflare R2 appeared first on Semaphore.

View Details

Traditional web hosting services, such as shared hosting, have long been the preferred choice for hosting sites and web applications. However, the web hosting landscape has evolved considerably in recent years. With advancements in technology and the rise of innovative platforms, numerous low-cost alternatives are now available for individuals and businesses seeking reliable hosting solutions.

In this article, you will understand what traditional web hosting services are, what they offer, and how much they cost. You will then explore five modern alternatives that offer affordability without compromising on performance or functionality.

Let’s dive in!

Traditional Web Hosting Services: Types, Cost, and ProvidersA web hosting service is an online service where a provider reserves space on its servers for storing and serving websites. This allows individuals or companies to host sites and make them publicly accessible on the Internet. Usually, these services can also host files, databases, and domain-specific mailboxes.

There are three types of traditional web hosting services:

  • Shared
  • Dedicated
  • Managed

Let’s dig into them, explaining what they offer, analyzing how much they cost, and mentioning some key providers.

Shared Hosting ServicesShared hosting providers share the resources of a single server among many different domains. This makes shared hosting a cost-effective solution for sites with limited traffic, such as blogs or small sites.

  • Cost range: A few dollars per month
  • Some well-known providers: Bluehost, HostGator, Hostinger, SiteGround, InMotion

Dedicated Hosting ServicesDedicated hosting providers assign one or more servers to each domain exclusively. In other words, servers are not shared with other websites or applications. That means greater performance, security, and control. However, it also implies that server management in on the customer.

Note that many shared hosting providers also offer dedicated hosting plans.

  • Cost range: From over $50 per month to hundreds of dollars per month
  • Some well-known providers: InMotion, Liquid Web, Bluehost, A2 Hosting, AccuWeb Hosting

Managed Hosting ServicesManaged hosting combines the benefits of dedicated hosting with expert management services. Unlike shared hosting, where multiple websites share the same server, managed hosting provides dedicated servers to individual companies or users. The hosting provider takes care of all server maintenance tasks, including security, software and hardware upgrades, and backups. Managed hosting is ideal for companies that want the reliability and performance of dedicated hosting without needing in-house expertise to maintain the server.

These services usually provide specific solutions such as Managed WordPress, Managed WooCommerce, Managed Magento, and the like.

  • Cost range: From over $20 per month to hundreds of dollars per month, depending on the managed technology
  • Some well-known providers: WP Engine, Kinsta, Liquid Web, EasyWP, tsoHost

Top 5 Inexpensive Alternatives to Web Hosting ServicesYou know that traditional hosting services range from a few dollars to hundreds of dollars per month. Aren’t there less expensive solutions? Of course, there are!

If you are eager to find out what these solutions are, take a look at the table below:

| Service | Cost | Number of sites | Ideal projects | Supported Sites | Supported Technologies | | --- | --- | --- | --- | --- | --- | | Free Hosting Services | Free | 1-5 | Small sites or non-commercial projects | Static sites and/or web applications | HTML, CSS, PHP, Node.js, React, Next.js, WordPress, and others | | Web Hosting via GitHub Pages | Free | One for each public repository | Non-commercial personal projects or static sites | Static sites and client-side sites | HTML, CSS, JavaScript, static site generators, some client-side technologies | | CDN Hosting | Free—$50+/mo | Potentially unlimited | Static sites | Static resources | Static HTML, CSS, JavaScript, and image files | | Cloud Hosting | <$1/mo—$1000+/mo | Potentially unlimited | Projects with fluctuating traffic | Static and dynamic sites, web applications, and more | Any | | VPS Hosting | $2+/mo—80+/mo | Potentially unlimited, but depends on the resources of the VPS | Projects needing variable resources | Static and dynamic sites, web applications, and more | Any |

Time to explore the five best alternatives to traditional web hosting services to save money!

Free Hosting ServicesFree hosting services offer space on their servers at no cost, typically for small sites and non-commercial projects. These providers usually offer free plans with limited resources, restricted bandwidth, and basic features, supporting only specific stacks like Node.js or traditional web development technologies like HTML, CSS, and PHP.

Some of the providers that offer free hosting services are:

  • InfinityFree: Supports sites based on PHP and MySQL.
  • 000WebHost: Supports sites based on PHP and MySQL.
  • Netlify: Supports web applications and sites built with React, Next.js, WordPress, and other technologies. It offers continuous deployment from GitHub.

💰 Cost range: Free

👍 Pros:

  • No financial investment required
  • Suitable for small or personal projects
  • Great for testing
  • Basic features and functionality for getting started
  • Ideal for beginners who do not want or cannot afford to pay for hosting

👎 Cons:

  • Limited storage, bandwidth, and customer support
  • Only a few technology stacks supported
  • Advertisements may be displayed on all pages

Web Hosting via GitHub PagesGitHub Pages is a free hosting service to host static sites directly from the files in a GitHub repository. It works with technologies like HTML, CSS, JavaScript, and natively supports Jekyll as a static site generator for blogging. Other static site generators like Hugo, Gatsby and similar can be used as well with some limitations. With some workarounds, it is also possible to deploy some client-side applications, such as React applications. As of this writing, GitHub Pages does not support server-side languages such as PHP, Node.js, Ruby, or Python.

By default, after publishing a site from a repository, it will be available at https://<username>.github.io/<repository> for individual users and https://<organization>.github.io/<repository> for organizations. However, you can also host GitHub Pages on custom domains as explained in the official guide.

Note that GitHub Pages is available for free in public repositories for both individual users and organizations. For more technical information, explore the GitHub Pages documentation.

💰 Cost range: Free for GitHub users

👍 Pros:

  • Free hosting with custom domain support
  • Built-in integration with GitHub for version control and automatic deploy at each push
  • Supports Jekyll natively
  • Automatic HTTPS for secure connections
  • Configuration in a few clicks

👎 Cons:

  • Limited to static content with no server-side processing capabilities
  • Requires knowledge of Git
  • Limited storage and bandwidth
  • Hosting sites from private repositories requires a Pro plan
  • Forbidden for commercial use

Content Delivery Network HostingA CDN (Content Delivery Network) is a network of servers distributed globally to strategically cover as many geographical locations as possible. Its purpose is to deliver web content efficiently to users, reducing latency, and speeding up loading times.

Specifically, a CDN caches and delivers resources such as HTML, CSS, JavaScript, and images from the server closest to the user. Thus, CDNs host static sites by distributing their files over a network of servers around the world to guarantee fast and reliable access to them.

These services usually offer pay-as-you-go plans where you pay per GiB of bandwidth and/or monthly plans in which you pay a fixed amount for a given level of service. This makes CDN hosting an inexpensive and efficient option for static sites. Most CDNs complete their offering with anti-bot protection, image optimization, and many other features.

Some popular CDN providers for hosting static sites are Cloudflare, Amazon CloudFront, Fastly, and Akamai.

💰 Cost range: Free, a few cents per GiB of bandwidth, up to $50+ per month, depending on traffic and/or enabled features

👍 Pros:

  • Improved latency times
  • Reduced resource load times
  • Enhanced global reach with servers worldwide
  • Scalability to handle traffic spikes
  • Extra features like DDoS protection, image optimization, anti-bot solutions, and more

👎 Cons:

  • Limited to static content, with no dynamic processing allowed
  • Complex DNS setup for beginners
  • May become expensive on sites with high traffic

Cloud HostingCloud hosting uses virtual servers in the cloud to host sites and web applications. It pools computing resources from a network of virtual and physical servers, offering high scalability and flexibility.

In most cases, cloud hosting works on a pay-as-you-go basis, which means that you pay only for the resources you use. That is especially beneficial for projects with unpredictable or fluctuating traffic, as there is no need to worry about overprovisioning or underprovisioning of resources.

Compared with traditional web hosting, cloud hosting can be more robust and reliable. This is because when a site hosted on a traditional provider needs more resources, you manually have to buy a new plan to get more computing power. Instead, cloud hosting automatically scales resources up or down for you.

Some popular cloud hosting providers are Amazon Web Services (AWS), Microsoft Azure, Google Cloud Platform (GCP), DigitalOcean, and Vercel.

💰 Cost range: From a few dollars a month to thousands of dollars a month, depending on usage

👍 Pros:

  • Automatic scalability to handle varying traffic levels
  • Pay-as-you-go pricing model for cost efficiency, especially on sites with fluctuating traffic
  • Redundancy and reliability through distributed infrastructure
  • Integration with CI/CD pipelines
  • Tons of other useful features and services

👎 Cons:

  • Requires technical expertise for setup and management
  • Traffic spikes can lead to unexpected costs
  • Vendor lock-in

Virtual Private Server HostingA VPS (Virtual Private Server) is a virtualized server environment created by splitting a physical server into multiple virtual servers. VPS hosting providers generally provide access to Linux or Windows virtual servers, with users who are tasked with configuring them from scratch for website hosting.

This hosting experience is similar to that offered by a traditional managed hosting service. The main difference is that VPSs are more flexible in that resources can be added or removed dynamically according to your needs, as they are virtual servers and not physical ones. Moreover, you typically pay based on the resources selected. Thus, in the long run, dedicated VPs can be a more cost-effective solution than managed hosting services.

Popular VPS providers include DigitalOcean, IONOS, InterServer, Vultr, and Linode.

💰 Cost range: From a few dollars per month to dozens of dollars per month, depending on how large the VPS is

👍 Pros:

  • Improved performance compared to shared hosting
  • Resource scalability
  • Root access for full control over server configuration
  • A single VPS can host several sites
  • Usually cheaper than managed hosting

👎 Cons:

  • Requires technical expertise for server management
  • Responsibility for security and updates
  • Maintenance and support might come at an additional cost
  • Security concerns as they are virtual servers

ConclusionIn this article, you learned what types of traditional web hosting services are and how much they cost. Fortunately, there are other low-cost modern solutions. Here, you dug into the five best alternatives to traditional web hosting solutions to save money without compromising on functionality. If you are an individual developer or small business, knowing these low-cost hosting solutions can save you a lot of money!

The post 5 Low-Cost Web Hosting Alternatives appeared first on Semaphore.

View Details

The rise of microservices, cloud platforms, containerization, and distributed systems has made computer systems incredibly powerful, but also increasingly intricate. Ensuring high availability in these complex environments requires innovative solutions. Enter Coroot, an open-source observability tool designed to simplify monitoring and prevent chaos in production. Coroot co-founders Peter Zaitsev and Nikolay Sivko shed light on their vision: empowering engineers to manage this complexity, maintain system uptime, and swiftly resolve issues.

Edited transcriptionWhat is Coroot?“We have a lot of open source and commercial tools that can help you in gathering telemetry data, building your dashboards, alerts, and so on, and we decided to look at the problem from different angles,” says Nikolay Sivko, CEO and co-founder of Coroot, an open-source observability tool. As systems are now more complex than ever (consisting of “microservices, architectures, hundreds or even thousands of services”), Coroot “aims to help engineers to manage this chaos in the production environment,” Nikolay explains.

To this end, Coroot is meant to provide not a cascade of insight, but just the right amount to help solve problems without overwhelming the user. As such, Coroot focuses on delivering actionable answers to user questions, providing information on what’s happening within the system, including errors and their root causes. In other words, Coroot’s bet is on actionability rather than comprehensiveness. “Being able to solve 95% of the problem is much better than having information to solve 100% of the problem, but being overwhelmed and not being able to do that,” says Coroot Co-founder Peter Zaitsev.

“The different approach here is working not with telemetry data directly, but with a model of a system,” says Nikolay. Coroot relies on fine-grained metrics, logs, and traces to build a comprehensive system model that takes into account all components working together, including containers, underlying nodes, network communication, and latency. “Analyzing such a model allows us to provide you with insights,” says Nikolay.

The Power of eBPFA core strength of Coroot is its ease of deployment, compared with other observability tools that require a complex installation and configuration for each specific use case. This is possible thanks to eBPF (Extended Berkeley Packet Filter). As Nikolay explains, “eBPF allows you to inject your small programs into any particular place of the kernel so we can instrument network connections, capture application-level queries, requests, and so on.”

eBPF works without code changes in the kernel itself. Its programs typically store captured data in a limited memory space (like a ring buffer) within the kernel. A separate user-space program then retrieves and processes this data, ensuring that even if the user-space program can’t keep up, the kernel buffer won’t overflow and crash the system.

What’s more, eBPF collects data with minimal performance impact, allowing Coroot to gather essential data with minimal setup, providing initial valuable insights “out of the box.” eBPF programs are verified before execution, guaranteeing they cannot introduce problems like infinite loops that could crash the system.

Embracing OpenTelemetrySimultaneously, Coroot uses OpenTelemetry, a vendor-neutral framework that allows users to avoid vendor lock-in with telemetry tools. OpenTelemetry simplifies collecting telemetry data (metrics, logs, traces) and sending it to various systems without code changes. According to Nikolay, implementing OpenTelemetry “is a good shift on the market because you can easily replicate your telemetry data flow into many systems and you don’t need to change your code anymore; once instrumented, it can be supported by many vendors, including Coroot.

Failure-Driven Development (FDD)Coroot uses a reactive development approach, where encountered failures inform future development: “We use failure-driven development because we love to reproduce failures and then build a product that can detect it,” says Nikolay. “We produce various scenarios of failure and build products that highlight the most time-consuming piece of your code and the root cause of errors from distributed cases,“ he adds.

On the other hand, the development team has no specific emphasis on unit tests. Unit tests might be written situationally. Due to the complex interactions between different Coroot components (node agent, exporters, core engine), integration testing is the primary focus.

Community and development“Now the majority of code is written by the team because we still are implementing our vision,” says Nikolay. This vision is still evolving but prioritizes features that directly address user needs. In this regard, Coroot plans to expand its deep-dive capabilities beyond Postgres, including highlighting resource-intensive queries in other databases and queue systems.

Still, while the core development is internal, Coroot welcomes contributions for smaller features like webhooks, plugins for specific databases, or integrations with other cloud providers. As for future features, Nikolay explains, “We are focusing now on simplifying the installation process because we want to provide users with a solution that can suggest to you what else you need to instrument.”

The bottom lineTrying Coroot is free and requires minimal setup. There’s a SaaS version available for those who prefer a cloud-based option and the possibility of self-hosting for those comfortable with Kubernetes. For a quick and easy setup, Nikolay and Peter recommend installing Coroot directly within your Kubernetes cluster using Helm.

The initial Coroot setup provides immediate value by offering insights into your entire system, including control plane components and all services, without additional configuration.

Additionally, Coroot welcomes feedback to refine its product vision. Users can engage through various channels:

  • Coroot website
  • GitHub repository
  • Slack community

While Coroot initially targeted Kubernetes environments, it has broadened its reach. The goal is to support various infrastructure components, including legacy databases, virtual machines, and managed services like AWS RDS and Elasticache: “We aim to support all the components to provide users with 100% coverage of the infrastructure because it’s super important to be able to see the big picture,” says Nikolay.

The post Peter Zaitsev and Nikolay Sivko on simplifying observability appeared first on Semaphore.

View Details

As discussed in the previous article, Docker secrets are a secure way to manage sensitive information in Dockerized environments. You got to see how you could incorporate secrets into compose files and also how you could manage secrets using the docker secret command.

As a follow-up to the previous article, we’ll be looking at three practical use cases of Docker secrets. We’ll go through how to use Docker secrets in a Dockerfile, manage WordPress secrets with Docker secrets, and roll back a secret. Through these practical examples, you will better understand how you can use Docker secrets in real-world scenarios.

PrerequisitesTo fully understand the hands-on examples in this article, you should have a basic understanding of Docker and Docker secrets. If you’re new to Docker secrets, you can check out the previous article to get started.

Additionally, you should have Docker installed on your machine preferably with Docker Swarm enabled.

Using Docker Secrets in a DockerfileIn some cases, you might need to incorporate secrets directly into your Docker image. This could be necessary when you need to build an image that requires a secret to be present at build time. In this example, we’ll look at incorporating a secret into a Docker image using a Dockerfile.

Step 1: Create a SecretFirst, you must create a secret that will be incorporated into the Docker image. You can create a secret using the docker secret createcommand.

For this tutorial, let’s say you have an AWS access key stored in a file named aws_credentials in your home directory ($HOME/.aws/credentials).

Step 2: Create a DockerfileCreate a Dockerfile in your project directory. This Dockerfile will define the steps needed to build your Docker image, including how to access and use the secret.

FROM your_base_image# Create a directory for the secretRUN mkdir -p /run/secrets# Copy the secret file into the containerCOPY $HOME/.aws/credentials /run/secrets/aws_credentials# Use the secret in your build processRUN --mount=type=secret,id=mytoken \ TOKEN=$(cat /run/secrets/aws_credentials) \ # Your build commands here Replace your_base_image with the base image you’re using for your Docker image.

This Dockerfile creates a directory for the secret, copies the secret file into the container, and then uses the secret in the build process. The --mount=type=secret,id=mytoken flag allows you to pass the secret to the build process securely.

In an actual scenario, you would replace the # Your build commands here with the specific commands needed to build your Docker image.

NOTE: As emphasized in the previous article, sharing secrets over version control systems like Git is not recommended. For this example, we’re assuming the secret file is stored locally on your machine, so it’s not shared with others.

Step 3: Build Your Docker ImageNow, build your Docker image using the docker build command. Make sure to use the --secret flag to pass the secret to the build process.

docker build --secret id=mytoken . This command will build your Docker image while securely passing the secret to the build process.

Step 4: Use the Secret in Your ApplicationYou can now use the secret in your application as needed. For example, if you’re using the AWS access key in your application to interact with AWS services, you can access it using the TOKEN environment variable set in the Dockerfile.

NOTE: Remember to replace mytoken with a meaningful identifier for your secret. Also, ensure that the secret file (aws_credentials in this example) is stored securely and is only accessible to authorized users.

Managing WordPress SecretsWordPress is a popular content management system that often requires sensitive information like database passwords to be securely managed.

In this example, we’ll create single-node MySQL and WordPress services using Docker secrets to manage sensitive credentials. We’ll set up a MySQL database for WordPress and configure WordPress to connect to MySQL.

OverviewThis tutorial can get a bit complex, so let’s break it down into steps:

  1. Generate random alphanumeric passwords for MySQL root and WordPress database.
  2. Create Docker secrets for MySQL and WordPress.
  3. Set up a MySQL service with Docker secrets.
  4. Configure a WordPress service to use MySQL with Docker secrets.
  5. Verify the services are running and test WordPress functionality.

Step 1: Generate Docker SecretsGenerate random passwords for MySQL root and WordPress database:

```

Generate MySQL root passwordopenssl rand -base64 20 | docker secret create mysql_root_password -# Generate WordPress database passwordopenssl rand -base64 20 | docker secret create mysql_password -

``` You should now have two Docker secrets with an output similar to:

uxm1a4sjjsg7enq2m1dtmvnon5judi7jm6d02xs99lj8061wul Step 2: Create MySQL ServiceCreate a MySQL service with Docker secrets:

docker network create -d overlay mysql_privatedocker service create \ --name mysql \ --replicas 1 \ --network mysql_private \ --mount type=volume,source=mydata,destination=/var/lib/mysql \ --secret source=mysql_root_password,target=mysql_root_password \ --secret source=mysql_password,target=mysql_password \ -e MYSQL_ROOT_PASSWORD_FILE="/run/secrets/mysql_root_password" \ -e MYSQL_PASSWORD_FILE="/run/secrets/mysql_password" \ -e MYSQL_USER="wordpress" \ -e MYSQL_DATABASE="wordpress" \ mysql:latest Your output should look similar to:

v87xhur2iqhwhvmql11345l7koverall progress: 1 out of 1 tasks 1/1: running [==================================================>] verify: Service converged This command creates a MySQL service with the specified secrets and environment variables. The MySQL service will be connected to the mysql_private overlay network.

Step 3: Create WordPress ServiceCreate a WordPress service that connects to MySQL using Docker secrets:

docker service create \ --name wordpress \ --replicas 1 \ --network mysql_private \ --publish published=30000,target=80 \ --mount type=volume,source=wpdata,destination=/var/www/html \ --secret source=mysql_password,target=wp_db_password \ -e WORDPRESS_DB_USER="wordpress" \ -e WORDPRESS_DB_PASSWORD_FILE="/run/secrets/wp_db_password" \ -e WORDPRESS_DB_HOST="mysql:3306" \ -e WORDPRESS_DB_NAME="wordpress" \ wordpress:latest You should see output similar to:

whoumrpbcry3exply4fl9euueoverall progress: 1 out of 1 tasks 1/1: running [==================================================>] verify: Service converged This command creates a WordPress service that connects to the MySQL service using Docker secrets. The WordPress service will be accessible on port 30000.

Step 4: Verify and TestFinally, verify that the services are running and test WordPress functionality:

```

Verify servicesdocker service ls# Test WordPressdocker service ps wordpress

``` You output should look similar to:

ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTSq2qzd1d2hup1 wordpress.1 wordpress:latest docker-desktop Running Running 2 minutes ago Access http://localhost:30000/ from any swarm node to set up WordPress using the web-based wizard. Verify that WordPress is operational and its state is saved across service restarts.

Figure 1:* WordPress Installation PageNOTE:* You need an active Docker Swarm to run the services. If you’re using Docker Desktop, you can enable Swarm mode by running docker swarm init.

Rotating Back a SecretRotating a secret means changing its value without affecting the services that use it. This is an important security practice to prevent unauthorized access to sensitive information.

This tutorial will demonstrate how to rotate a secret in Docker, building upon the previous use case. We’ll create a new secret with a new MySQL password, update the MySQL and WordPress services to use it, and then remove the old secret.

Step 1: Create a New SecretGenerate a new password and store it as a secret named mysql_password_v2:

openssl rand -base64 20 | docker secret create mysql_password_v2 -vwqvnkm1gploicqk2g32vwpf2 Step 2: Update ServicesUpdate the MySQL service to grant it access to the old and new secrets. Remove the old secret and add the new one:

docker service update \ --secret-rm mysql_password mysqldocker service update \ --secret-add source=mysql_password,target=old_mysql_password \ --secret-add source=mysql_password_v2,target=mysql_password \ mysql You should see output similar to:

mysqloverall progress: 1 out of 1 tasks 1/1: running [==================================================>] verify: Waiting 5 seconds to verify that tasks are stable... service update paused: update paused due to failure or early termination of task f98yfogvf606ddrdc0z3b1pafmysqloverall progress: 1 out of 1 tasks 1/1: running [==================================================>] verify: Service converged This command updates the MySQL service to use the new secret while keeping the old secret for backward compatibility.

Step 3: Change MySQL PasswordChange the MySQL password for the WordPress user using the mysqladmin CLI. This command reads the old and new passwords from the secret files:

docker container exec $(docker ps --filter name=mysql -q) \ bash -c 'mysqladmin --user=wordpress --password="$(< /run/secrets/old_mysql_password)" password "$(< /run/secrets/mysql_password)"' You should see output similar to:

mysqladmin: [Warning] Using a password on the command line interface can be insecure.Warning: Since password will be sent to server in plain text, use ssl connection to ensure password safety. This command changes the MySQL password for the WordPress user to the new password.

Step 4: Update WordPress ServiceUpdate the WordPress service to use the new password. Trigger a rolling restart of the service:

docker service update \ --secret-rm mysql_password \ --secret-add source=mysql_password_v2,target=wp_db_password \ wordpress

You should see output similar to:bashwordpressoverall progress: 1 out of 1 tasks 1/1: running [==================================================>] verify: Service converged This command updates the WordPress service to use the new secret and removes the old secret. Step 5: Verify FunctionalityOn the WordPress site, verify that you can still access and interact with the site as expected. Ensure that the MySQL password change did not affect the WordPress functionality.

NOTE: You might be wondering how all this can work in CI/CD pipelines such as Semaphore. To make this possible, you’ll need scripts. So, instead of manually running these commands on your machine, the CI/CD pipeline will execute this script and commands automatically whenever there is a new code push or a trigger event. To try it out, check out this GitHub repository that goes through this process.

However, Semaphore has a much simpler way of including secrets into your project. You can check this page on creating and managing secrets to know more.

Step 6: Revoke Access and CleanupAfter verifying that the services are operational, revoke access to the old secret and clean up the old secret:

docker service update \ --secret-rm mysql_password \ mysqldocker secret rm mysql_password

You should see output similar to:bashmysqloverall progress: 1 out of 1 tasks 1/1: running [==================================================>] verify: Service converged mysql_password ConclusionIn this article, we looked at three practical use cases of Docker secrets. We explored how to incorporate secrets into a Docker image using a Dockerfile, manage WordPress secrets using Docker secrets, and rotate a secret in Docker.

With these examples, using secrets in various scenarios becomes more straightforward. You can now apply Docker secrets to your projects to securely manage sensitive information and improve the security of your Dockerized applications.

The post Three Practical Use Cases of Docker Secrets appeared first on Semaphore.

View Details

According to Gartner estimates, by 2027 more than 90% of global organizations will be running containerized applications in production. A significant increase from fewer than 40% in 2021. As containerization gains traction and becomes the de facto standard for deploying applications, Kubernetes is seeing a sharp increase in usage across enterprises.

Having said that, a recent survey conducted by CNCF in partnership with FinOps foundation revealed an increase in Kubernetes cloud cost across the board during 2023. A surprising finding is that most respondents needed help tracking and predicting Kubernetes cost-effectively. A wise man once said, “A penny saved is a penny earned,” but measuring costs correctly in Kubernetes environments is complex.

This article discusses best practices and tools for implementing cost optimization and gaining accurate visibility for your Kubernetes workloads.

Challenges in Managing Kubernetes Cloud CostLet’s quickly take a look at some of the challenges of managing costs with Kubernetes:

  • Kubernetes workloads are dynamic, making it difficult to accurately calculate the cost of the resources used by your apps.
  • We frequently deploy applications to a multi-tenant Kubernetes cloud cluster, which complicates accurately estimating resource costs. While cloud vendors make it relatively easy to know that your Kubernetes cluster is costing you $100 per day, it can be difficult to estimate how many resources, i.e. compute and memory, a team or project is using.
  • Even if everything is reduced to CPU, memory, and load balancers, various external considerations such as shared resources and cluster management fees can complicate your Kubernetes cluster cost estimation.
  • Overprovisioning of resources and a lack of awareness and responsibility among individuals and teams can increase the cost estimate of your Kubernetes cluster cloud. Overprovisioning can also lead to the tragedy of commons situation where there is no incentive to be modest while dimensioning deployments.
  • In the absence of standardized processes and tools to visualize Kubernetes cost, budgeting or spending analysis becomes difficult.
  • Sometimes, when you are unsure about your workload consumption trends, which fluctuate often, it is hard to get accurate information on Kubernetes cloud cost.

Splitting Down CostsKnowing how much your managed Kubernetes cloud cluster will cost ahead of time is preferable to being surprised when you receive your monthly bills. The following components make up the typical cost of running an application on a managed Kubernetes cloud cluster:

  • CPU and memory
  • Persistent storage
  • Ingress/Egress
  • Network
  • Common services (i.e., control plane and service mesh)
  • Cluster management fee

How Cloud Providers ChargeYour organization’s decision to harness the power of Kubernetes depends on a set of parameters, and cost is crucial. But do you know how different cloud providers charge you for managed Kubernetes instances?

Every cloud provider charges you differently for the managed control plane. For example, Digital Ocean comes with a free control plane. This free service includes management for processes like etcd, kube-apiserver, kube-controller-manager, kube-scheduler, cloud-controller-manager, and other services. Civo is another managed Kubernetes service vendor that offers the control plane for free

While on the other hand, Google Kubernetes Engine and AWS Elastic Kubernetes Service charge for the control plane usage. Keeping track of all factors impacting Kubernetes cluster cost can be overwhelming.

Strategies to Manage Kubernetes Cloud CostsThe following are some best practices teams can follow to optimize their Kubernetes cloud costs.

  • Right-sizing your Kubernetes cluster nodes is crucial in optimizing resource utilization. If your containers are too large and consume most of your node resources, adding more containers on that node would be a task.
  • You should right-size pods using Kubernetes resource requests and limits by specifying the minimum and maximum amount of resources your application can request.
  • Leveraging autoscaling and downscaling features of Kubernetes using the horizontal pod autoscaler and cluster autoscalercan drastically reduce your cloud expenditures.
  • Kubernetes resource bin packing is another technique you should consider for cost-efficient utilization of your infrastructure. In this technique, multiple containers are consolidated into fewer nodes to increase the utilization of your cluster nodes.
  • Cloud discounts (i.e., AWS saving plans)
  • Startup programs (i.e., Y Combinator companies get $100k in GCP credits)
  • If your Kubernetes workloads are stateless, you can run them on spot instances to save cost.
  • Negotiate and ask for volume-based discounts

Tools to Manage Kubernetes CostsAscertaining and managing your Kubernetes costs is a complex task, and you need multiple tools in your arsenal to do some of the heavy lifting.

Cast AICAST AI is a Kubernetes automation platform that claims to cut cloud costs by half.

  • CAST AI can scan your Kubernetes cluster within a few minutes and give you a cost-saving report to know how much you could have saved using its features. Furthermore, if you agree with the report’s findings, you can structure CAST AI to implement those changes to cut costs immediately and automatically.
  • CAST AI also has an AI-driven instance selection mechanism that, based on your requirements, decides on the best instance type at the minimum cost.
  • CAST AI has bin packing features that allow you to compact pods in fewer nodes and later on delete nodes when they no longer have workloads scheduled on them. In this approach, your nodes reach high utilization, and you don’t have to pay for resources that aren’t utilized.

Kube-downscalerkube-downscaler is an open-source framework that can reduce your Kubernetes resources to zero when they are not being used actively.

  • kube-downscaler scales down Kubernetes resources during non-working hours. It’s like Thanos snapping his fingers, and all your Kubernetes pods are effectively scaled down to zero.
  • You can define schedules based on your usage pattern, and then it will scale down your cluster. For example, you can scale down the cluster on weekends.
  • It is great for your lower environments, where you might not need the infrastructure to be available 24/7.

HarnessHarness is a modern software delivery platform powered by AI. It has built-in capabilities to manage your cloud bills with its Cloud Cost Management feature.

  • Harness provides in-depth, actionable insights into your dynamic idle Kubernetes resources by effectively shutting down non-production idle resources with its cloud AutoStopping feature. Harness claims that by using this feature, teams can save up to 70% on non-production cloud costs, all with a simple one-time setup that doesn’t need maintenance.
  • Estimate how much of your existing cloud costs you can save using its savings calculator.
  • Detecting and flagging anomalies in your cloud consumptions by analyzing historical spending patterns.

KubecostKubecost is the most popular open-source tool for getting a real-time bird’s eye view of Kubernetes costs. It uses preconfigured Grafana dashboards with its opinionated setup (tailored to your needs) to monitor Kubernetes cluster costs. You can install it using a Helm chart.

  • It is a unified Kubernetes cost-monitoring solution that supports all major cloud providers.
  • Automatically generates Kubernetes cost optimization recommendations via its UI or API. These recommendations can be dynamically applied for immediate results.
  • Provides real-time alerting capabilities so that teams can take control of their Kubernetes-related costs and stay within budgeted limits.
  • Breaks down the cost allocation to Kubernetes resources such as deployments, namespaces, labels, pods, and containers (but not limited to these). You can transparently and accurately share the cost among your teams and tally it with your cloud bills.

OpenCostOpenCost is an open-source cost monitoring tool for cloud-native environments. It is a CNCF sandbox project created by Kubecost.

  • If you have budget constraints, this is the only tool built by Kubernetes experts and practitioners that is freely available in the market.
  • It relies on vendor-neutral OpenCost Specification to measure and allocate Kubernetes clusters’ infrastructure and container costs.
  • OpenCost API enables real-time and historical reporting on Kubernetes cloud costs based on on-demand list pricing.

LoftLoft’s Kubernetes platform has excellent features to help you manage resource costs.

  • Sleep mode can scale down your Kubernetes environment by putting namespaces to sleep and purging all pods inside a namespace during periods of inactivity. Kubernetes resource configuration is not purged and remains in the cluster.
  • Auto-delete, as the name implies, deletes namespaces and virtual clusters that have been idle, old, or unused for some time.

Harness Case StudyRelativity, a SaaS data company, reduced its cloud costs by millions in five months using Harness cloud cost management features. Relativity doubled the density of Kubernetes pods per node from 40 to 100 by analyzing the poor utilization of Kubernetes pods operating on cloud infrastructure (nodes), and in some large nodes, 100 pods per node. The financial impact of this was a 40% reduction in Kubernetes costs per day. They saved one full-time employee from the expenditures of developing an in-house cost management platform for product and engineering teams using Harness Cloud Cost Management.

Final ThoughtsWhile it is customary for engineering teams to overprovision resources to run their Kubernetes clusters, this approach may not be sustainable—someone must pay in the end. So, whether you are a seasoned expert or just starting with Kubernetes, you must consider the costs of maintaining a Kubernetes cluster.

The post Optimizing Kubernetes Cloud Costs appeared first on Semaphore.

View Details

UI rendering and animations are what give an app a great and interactive user experience. As interesting as this concept is, so is how frustrating it can become if things go wrong. And one way that could happen is tests being flaky.

There are actually multiple reasons for flaky tests in rendering or animations, but one of the main reasons is timing.

For instance, you might have a spring animation of a button displaying. Now testing this might result in flakiness, all because the animation finished some milliseconds too late due to a random background running process.

In this guide, we’ll be using Jest and React testing libraries to describe real-world examples of tests for better understanding. However, you can follow along, irrespective of your stack.

Root causesFlaky tests, in general, have quite a number of common causes. However, when being specific to UI rendering and animations, here are the main root causes:

Timing IssuesAnimations by default often come with setTimeout delays or transition-duration properties. These timings are sometimes what make assertions fail resulting in flakiness.

Take a look at this test for a component with an animation whereby its width gets increased with the click of a button:

test("Box width increases", () => { render(<Box />); const box = screen.getByTestId("box"); expect(box).toHaveStyle({ width: "70px" }); fireEvent.click(screen.getByRole("button")); expect(box).toHaveStyle({ width: "100px" });}); The problem right here is that after clicking the button to increase the width, the assertion might execute before the animation of the width increase finishes depending on any timing events.

Thus, the main challenge in rendering and animation testing comes down to making sure that assertions are made after the animation has fully executed and not earlier. A nice approach would be to wait for the animation to complete; in React testing, this can be achieved with the use of waitFor.

Environmental DependenciesThe system resources come into play in this case. In a way, tests created for rendering complex animations can be dependent on system resources like CPU or memory.

Now this won’t be an issue for machines that are capable. However, if the machine has limited resources, animation rendering could be slow and less smooth, thus triggering random test failures.

Another instance of environmental dependencies can be network connectivity. Let’s say you have an animation that relies on fetching external assets; this alone introduces unexpected factors as network fluctuations can influence the animation behavior. A way to fix this would be with the use of mocks.

Libraries DependenciesOften times, animations used are from libraries, as it is much faster and more efficient than writing the code from scratch.

In React for instance, there are tons of animation libraries like Framer Motion, React Spring, etc. However, these libraries could cause flaky tests when:

  • the version of the third-party library and the current version of the technology stack used are not compatible. This case is sometimes silent, as errors might not be displayed, just that the tests results seems to be unstable.
  • the animation libraries used have dependencies that interfere with one another.
  • the animation libraries have internal bugs that haven’t been fixed by the library developers which only show up during testing.
  • developers use animation libraries in a way it wasn’t designed for. Interestingly enough, the library might perform the task as expected but since it was used incorrectly, testing can provide inconsistent results.

Fixing Flaky Tests in Rendering and AnimationNow, to make this guide more elaborate and easier to understand, we’ll provide four real-world code examples and show the wrong way causing flaky tests and the right way to fix them.

Rendering of a Newsletter PromptIn this example, this component renders a simple form with an email input and a subscribe button. When submitted, it logs the email and resets the state.

function NewsletterPrompt() { const [email, setEmail] = useState(""); const [subscribed, setSubscribed] = useState(false); const handleSubmit = (e) => { e.preventDefault(); setSubscribed(true); }; return ( <div> {!subscribed ? ( <div> <h2>Subscribe to Newsletter</h2> <input type="email" placeholder="Email address..." value={email} onChange={(e) => setEmail(e.target.value)} /> <button type="submit" onClick={handleSubmit}> Subscribe </button> </div> ) : ( <p>Subscribed successfully</p> )} </div> );} Now let’s write a test for the component that’ll show how flakiness can happen:

test("Newsletter prompt renders and can be subscribed to", () => { render(<NewsletterPrompt />); const emailInput = screen.getByPlaceholderText("Email address..."); const subscribeButton = screen.getByText("Subscribe"); fireEvent.change(emailInput, { target: { value: "xyz@example.com" } }); fireEvent.click(subscribeButton); expect(screen.getByText("Subscribed successfully")).toBeInTheDocument();}); The problem here is that making assertions directly like this could run before the component even re-renders to show updates of the submitted email.

This test is bound to be flaky because it relies on the component being rendered synchronously, which might not always be the case. As rendering can sometimes be asynchronous.

A fix for this test would be something like this:

test("Newsletter prompt renders and can be subscribed to", async () => { render(<NewsletterPrompt />); const emailInput = screen.getByPlaceholderText("Email address..."); const subscribeButton = screen.getByText("Subscribe"); fireEvent.change(emailInput, { target: { value: "xyz@example.com" } }); fireEvent.click(subscribeButton); expect( await screen.findByText("Subscribed successfully"), ).toBeInTheDocument();}); In this fix, we used async/await and the screen.findByText method to expect and wait for the text content element to show up in the DOM, which would prove that the component has indeed re-rendered with the “Subscribed successfully” message.

An Animated ButtonLet’s say you have a button that animates its color from orange to blue when clicked and reverts to orange when clicked again.

export default function AnimatedButton() { const [isAnimated, setIsAnimated] = useState(false); const handleClick = () => { setIsAnimated(!isAnimated); }; const buttonStyle = { width: "100px", height: "50px", backgroundColor: isAnimated ? "blue" : "orange", transition: "backgroundColor 0.5s ease", }; return ( <button style={buttonStyle} onClick={handleClick}> Animate </button> );} Now here’s a common but incorrect way to write the test for this button color change, as it could show flaky behavior:

import { render, fireEvent } from "@testing-library/react";import AnimatedButton from "./AnimatedButton";test("background color changes", () => { render(<AnimatedButton />); const button = screen.getByRole("button", { name: "Animate" }); fireEvent.click(button); expect(button).toHaveStyle({ backgroundColor: "blue" }); fireEvent.click(button); expect(button).toHaveStyle({ backgroundColor: "orange" });}); This test looks really straightforward; it renders the button, clicks it, and then asserts the expected color changes. However, there’s a bit of a weak spot, which is where flakiness comes in.

The reason this test might sometimes show flaky results is that after each click event, it assumes that the animation completes immediately, as we aren’t considering the timing of the animation before asserting the state of the button’s background color.

Instead, this would be the right way:

test("background color changes", async () => { render(<AnimatedButton />); const button = screen.getByRole("button", { name: "Animate" }); fireEvent.click(button); await waitFor(() => expect(button).toHaveStyle({ backgroundColor: "blue" })); fireEvent.click(button); await waitFor(() => expect(button).toHaveStyle({ backgroundColor: "orange" }), );}); With this simple change of using waitFor, we can be certain that the test would allow the animation to be completed before asserting the background color of the button. This makes sure that the test assertion is in sync with the UI’s rendering update.

Rendering of a Basic ModalFor this example, let’s use a simple modal component that when a button is clicked, the visibility of the modal changes:

function AnimatedModal() { const [isVisible, setIsVisible] = useState(false); const toggleVisibility = () => setIsVisible(!isVisible); return ( <div> <button onClick={toggleVisibility}>Toggle Modal</button> {isVisible && <div data-testid="animated-modal">Animated Modal</div>} </div> );} Now as simple as this component is, testing if it renders correctly can become a hassle because of the animation.

Here is a wrong way to write the test:

test("Toggling animated modal", () => { render(<AnimatedModal />); const toggleButton = screen.getByRole("button", { name: "Toggle Modal" }); expect(screen.queryByTestId("animated-modal")).not.toBeInTheDocument(); fireEvent.click(toggleButton); expect(screen.getByTestId("animated-modal")).toBeInTheDocument();}); As we explained in the previous example, this test also asserts the presence of the modal content immediately after the click event. So due to the animations being asynchronous, this test is bound to be a flaky one.

Here’s the right way:

test("Toggling animated modal", async () => { render(<AnimatedModal />); const toggleButton = screen.getByRole("button", { name: "Toggle Modal" }); expect(screen.queryByTestId("animated-modal")).not.toBeInTheDocument(); fireEvent.click(toggleButton); await waitFor(() => { expect(screen.queryByTestId("animated-modal")).toBeVisible(); }); fireEvent.click(toggleButton); await waitFor(() => { expect(screen.queryByTestId("animated-modal")).not.toBeInTheDocument(); });}); In this fix, we used waitFor to assert the modal’s presence or absence visibility only after the animation has been completed. This gives us the certainty that the test is more reliable.

Strategies to Reduce Flaky Tests in Rendering and AnimationSo when writing tests specifically for UI rendering and animations, here are some things to look out for or what you should use instead to reduce the chance of flaky tests showing up:

  • Always review the test logic written as it is one of the common mistakes made as explained in the section above.
  • Network delays or fluctuations should be accounted for and mock functions can come in handy in fixing this.
  • When testing on UI rendering, avoid using DOM selectors like .querySelector() to get elements, instead use the queries provided by the testing library like getBy, queryBy, or findBy.
  • Ensure that the version of the animation library used is compatible with the version of the tech stack used.
  • Keep the rendering or animation tests in smaller units i.e. try to only test one component at a time. This is more efficient and easier to debug flakiness.
  • Know when to use the appropriate assertion matchers, for instance when testing animations, toBeVisible() is better suited than toBeInTheDocument().
  • Use waitFor for assertions and not for firing events.
  • You can use jest.useFakeTimers() as an alternative to waitFor for further control of testing.
  • Some developers skip/disable animations when testing, while it’s not recommended, it is an option.

ConclusionFlaky tests in rendering and animations can actually be difficult to deal with but with a proper understanding of the common causes specifically timing complications, the flakiness of tests can be reduced tremendously.

For further reading on flaky tests, check out these resources:

  • Flaky Tests In React: Detection, Prevention and Tools
  • Best Practices for Identifying and Mitigating Flaky Tests
  • How to Avoid Flaky Tests in Playwright

The post How To Fix Flaky Tests in Rendering and Animation Workflows appeared first on Semaphore.

View Details

In software development, an application is only as good as its documentation. Good technical documentation requires a lot of work but is one of the best investments a company can make.

To keep up with the need for quick and accurate guides, companies, today, are using AI tools for help. So, in this article, we’ll take a look at three AI documentation generators, check out what they offer, and weigh their pros and cons.

Three Tools for Automatic Software DocumentationIn this section, we’ll explore the capabilities of three prominent AI-driven documentation generators:

  • Scribe
  • DocuWriter
  • Bito AI documentation generator

We’ll explore their features and make a comparison among them to enlighten similarities, differences, and use cases.

ScribeScribe is an open source, AI-powered documentation generator.

This tool turns any process into step-by-step tutorials along with text, code snippets, internal links, and screenshots. So, the ideal usage of this tool is to create processes on the fly, as you go.

For example: do you know that Excel formula you use barely every day but it’s so complicated you never remember it? Well, here’s where Scribe comes into handy: while you make the process, Scribe creates the documentation for you. Once and for all.

No more screenshots, GIFs, or videos scattered here and there: Scribe creates the documentation as you make the process, making it consistent.

Check out how easy is to create (and share!) documentation with Scribe here.

DocuWriterDocuWriter it’s an AI-powered documentation generator that creates code and API documentation from your source code files.

In particular, DocuWriter uses AI to generate the documentation for your code, allowing you to save time while having consistent documentation, updated with the latest changes.

The power of DocuWriter not only relies on automatically creating the documentation. It also creates tests and code refactors from your source code, allowing you to concentrate only on what matters: solving problems through your favorite programming language.

Last but not least, you can use it directly in VS CODE, thanks to the dedicated integration.

Bito AI Documentation GeneratorBito documentation generator is an AI-automated software documentation generator that ensures the documentation is always up-to-date with the latest code changes.

By Providing Bito a directory, it will automatically generate a detailed overview, visualization, and documentation for each file including:

  • Summary of the file
  • Dependencies
  • Documentation regarding classes, modules, functions, and methods.

If the execution of Bito is successful, the output provided creates a directory that includes:

  • Modules documentation. These are individual markdown files for each module, that explain the module’s purpose, functions, and interactions.
  • Aggregated documentation. This is a comprehensive markdown file that consolidates the documentation from each module. This file also includes flow maps and visuals.

A Comparison Among ThemAs not all the tools are useful for every scenario, in this section we want to make a comparison among these three documentation generators to understand some use cases they might be better suitable for.

Scribe is particularly useful to document processes and procedures. Thanks to how it is developed, in fact, it registers the movements the mouse is making on the screen during the process and it creates a step-by-step guide out of it.

This comes in handy in different situations like:

  • Troubleshooting.
  • Processes and procedures, both internal and external to the company you’re working for.

So, Scribe may be useful for non-developer users who need to create process documentation but want to use software that speeds the process up, avoiding the creation of images and videos for, then, creating the structure elsewhere (like in Microsoft Word or Confluence, as it typically happens).

DocuWriter, on the other hand, is a documentation generator specifically suited for a developer audience, helping it automatically create the documentation for their APIs.

As there are a lot of documentation generators that create API documentation, DocuWriter emerges for the following features:

  • It creates tests and code refactors from the code files, not only the documentation.
  • It can be integrated into VS CODE.
  • It provides documentation both in markdown and PDF.
  • It creates the documentation for your preferred programming language, without any exceptions.

Even Bito is particularly suited for a developer audience. The main difference between Bito and DocuWriter is that DocuWriter created the documentation for APIs while Bito created the documentation related to the source code of your project providing:

  • Documentation for modules and functions and their interactions.
  • Visuals and flow maps to represent modules’ interactions.
  • The possibility to ignore directories during the process of creating the documentation.

Pros and Cons of Using AI Documentation ToolsAs AI is dominating almost every scenario and industry today – at least, in IT – it doesn’t mean that an AI tool is the perfect solution for your needs.

In fact, documentation generators aren’t always the right solution to your documentation problems. So, here we want to explore the pros and cons of using AI documentation generators.

Pros:

Among the pros, we can consider the following:

  • Efficiency. AI documentation generators can automatically generate comprehensive documents much faster than manual writing, saving significant time and resources.
  • Consistency. These tools ensure that the documentation is consistent in style and terminology, which is crucial for maintaining the quality and clarity of user guides across different parts of the software.
  • Scalability. AI-powered tools can easily update and expand documentation to keep pace with software updates and new features without starting from scratch each time.

Cons:

Among the cons, we can consider the following:

  • Lack of Deep Understanding. AI may not fully grasp the nuances of user needs and the contextual use of the software, which can lead to less user-friendly documentation.
  • Over-Reliance on Inputs. The quality of AI-generated documentation heavily depends on the quality of the inputs (code annotations, comments, etc.). Poor inputs can result in poor outputs.
  • Generic Content. AI might generate documentation that is technically accurate but generic and not addressed to a specific audience. This means that it may be too specific or too poor, depending on the intended audience.

ConclusionsIn this article, we’ve described three AI documentation generators along with their features and a comparison among them.

While we understand the power of AI, a piece of advice we’d like to give is the following: if you’re considering using an AI documentation generator, consider also collaborating with an experienced technical Writer. In fact, as AI can speed up the process, the intervention of an experienced human is the touch that makes the portrait perfect to suites your specific needs.

The post Unlocking Efficiency: 3 AI Tools for Automatic Software Documentation appeared first on Semaphore.

View Details

On a modern site, each web page typically retrieves data by contacting online services and backends via API. Considering the popularity of the microservices approach, your site is likely to rely on several external APIs.

Calling an API in a Next.js component is simple and takes only a few lines of code. The problem is that the application may end up containing the same logic for making an API call in multiple places. This introduces code duplication issues and makes your codebase less maintainable. With the API layer approach presented in this article, you can address all those downsides.

Suppose we have a layer that contains everything needed to call all the APIs on which the site depends. Encapsulating all the logic for making API calls in the same place would make it easier to update an endpoint string or request behavior. This is the main goal of an API layer.

Let’s find out what a Next.js API layer is, why your site needs one, and how to implement it using the fetch() HTTP client.

What Is an API Layer?An API layer is the part of your Next.js Application that contains the Programming logic for retrieving data via some Interface (API) on the server. In detail, it centralizes all API calls your server components make to fetch data from external services and backends.

All the logic required to build this architectural layer is encapsulated in the api folder. This consists of several files with the following naming convention:

<externalService>API.js The <externalService> qualifier facilitates the organization of all APIs that reference the same external service or backend in the same file. Note that a Next.js API layer may also involve some utility and configuration files. These do not follow the naming convention above.

Here is what a sample API layer in a Next.js application may look like:

api├── configs│ └── ...│├── AuthorAPI.js...├── ProductAPI.js...└── UserAPI.js Now, you can make an API call in a Next.js server component using the API layer as in the example below:

import { ProductAPI } from "@/api/PokemonAPI"export default async ProductListComponent() { // retrieve the desired data using the API layer const products = await ProductAPI.getAll(); // presenting the product list ... return <div>...</div>} ProductListComponent is a server component. As such, ProductAPI.getAll() will generate a server-to-server HTTP request that will be performed by the Next.js server at page generation or build time.

Let’s now dig into the reasons why you need an API layer in a Next.js application.

Why Your Next.js Project Needs an API LayerAdopting an API layer can lead to several benefits in your Next.js architecture. Explore the three most important ones.

  1. Avoid code duplicationIn a Next.js App Router application, top-level page components usually consist of several server components. These retrieve data to be rendered in the HTML by making requests to API endpoints.

As different server components may need the same data, you end up pasting the following logic pattern calling the same API throughout your codebase:

// the logic required to retrieve all products// with fetch() via APIconst productsResponse = await fetch("https://your-cms.com/api/v1/products");const productsData = await productsReponse.json();const products = productsData.results; Each time you have to retrieve the list of all products from the CMS via API, you will have to repeat those three lines of code. As you can imagine, this approach leads to code duplication.

By encapsulating API calls into reusable functions within API layer files, you can avoid writing boilerplate duplicate code. The three lines of repeated code above will become this more readable and maintainable single line of code:

const products = await CmsAPI.getProducts(); 2. Centralize all API request logic in the same placeAdopting an API layer enables the centralization of all the logic needed to call APIs in the same place, which has several advantages. For example, suppose the endpoint of an API changes (e.g., from /v1/posts to /v1/articles).

To address that change in the code with an API layer, you would just need to update the endpoint string in the fetch() call inside the specific API function. All API calls referred to that endpoint will now work as desired. This is the main benefit of logic centralization. Without an API layer, you would have to search for all occurrences of the endpoint in your codebase and manually update each one instead. What a tedious and error-prone task!

Also, the centralization offered by the API layer simplifies configuration. This means that if you suddenly need to pass new header values or cookies in some requests, you would only have to modify some API functions accordingly.

  1. Deal with data caching easilyStarting with version 13, Next.js comes with a special implementation of the fetch() function from the Fetch API. In addition to all the features fetch() offers in the browser and Node.js, this implementation allows each server-side request to specify its own persistent caching behavior.

In other words, any HTTP request made with the Next.js fetch() function inside server components will produce a result that can be cached on a persistent cache on the server. This cache stores data so that it does not have to be retrieved again on the same request. The server-side caching mechanism leads to reduced network usage, improved page generation time, and improved server response time. Learn more in our guide on how to build a caching layer in Node.js.

By default, Next.js automatically caches the data returned by fetch() requests in the Data Cache on the server. This behavior depends on the optional cache option:

fetch(`https://...`, { cache: "force-cache" | "no-store" }) The possible values are:

  • force-cache: Before making the request, Next.js looks for a matching response in its cache. If there is a match and it is fresh, it returns that data. Otherwise, the server performs the request and updates the cache with the updated response. This is the default value.
  • no-store: Next.js ignores the cache, fetching the resource on every request and not updating the cache with the retrieved content.

Another interesting option is next.revalidate:

fetch(`https://...`, { next: { revalidate: false | 0 | number } }) This sets the cache lifetime of a resource. The allowed values are:

  • false: Cache the resource indefinitely. This is the default value.
  • 0: Prevent the resource from being cached.
  • number: Specify the number of seconds the resource should be in the cache.

Find out more about data caching with Next.js in the official documentation.

By implementing the API layer with the Promise-based fetch() HTTP client, you can gain access to all these powerful caching options.

Implement an API Layer in Next.js With fetchFollow this tutorial section and learn how to build an API layer in a Next.js application based on the App Router. If your Next.js application uses the Pages Router instead, you can follow the same approach we introduced in guide on how to implement a Node.js API layer.

Do not forget that the API layer implemented here will be used to retrieve data when generating pages on the server. To dynamically retrieve data on the frontend, you can integrate your Next.js application with a React API layer.

Time to build an API layer in Next.js using fetch()!

PrerequisitesFirst, you need a Next.js 13+ application using the App Router approach. You can create one with create-next-app as follows:

npx create-next-app@latest During the initialization process, make sure “Yes” to the following question:

Would you like to use App Router? (recommended) » No / Yes As mentioned before, the Promise-based HTTP client used to implement the Next.js API layer will be the fetch() utility. That function is part of the Next.js core API, so you do not need to install any external dependencies.

Add the API layer to your architectureLet’s assume that your Next.js App Router project is based on the recommended /src directory. The file structure of your project will be something like this:

└── src│ └── app│ ├── favicon.ico│ ├── globals.css│ ├── layout.js│ ├── page.js│ └── page.module.css├── public│ ├── next.svg│ └── vercel.svg├── .eslintrc.json├── .gitignore├── jsconfig.json├── next.config.mjs├── package-lock.json├── package.json└── README.md Prepare your application to host an API layer by adding an api folder inside /src. The new project structure will be:

└── src│ └── api│ └── ...│ └── app│ └── ...... The api subdirectory will contain all the logic required to perform server-side API calls in your Next.js application.

Implement the API layer with caching logicSuppose your Next.js site relies on a headless CMS technology, which exposes public endpoints you need to contact to retrieve the data required to generate web pages on the server or at build time.

You can organize all those endpoints in a cmsAPI.js API layer file inside the api folder as follows:

// src/api/cmsAPI.js// common variable to use in all API object functionsconst baseUrl = "https://your-cms.com/api/v1";// other common logic...export const CmsAPI = { get: async function (slug, options = undefined) { const response = await fetch(`${baseUrl}/article/${slug}`, { method: "GET", ...options, }); return await response.json(); }, getArticles: async function ( { limit = 10, offset = 0 }, options = undefined ) { const queryParams = new URLSearchParams({ limit: limit, offset: offset, }); const response = await cmsClient.request( `${baseUrl}/articles?${queryParams}`, { method: "GET", ...options, } ); return await response.json(); }, getAllDrafts: async function () { const response = await cmsClient.request(`${baseUrl}/articles/drafts`, { method: "GET", ...options, }); return await response.json(); }, // other CMS APIs ...}; This is a real-world example of an <externalService>API.js API layer file. As you can see, it is nothing more than a JavaScript object that exposes functions corresponding to the public endpoints exposed by the CMS. In particular, each object function uses fetch() to contact a specific CMS endpoint and retrieve the desired data.

Notice that each API function accepts an optional options argument. That is required because the Next.js fetch() method supports all the options of the fetch() implementation in the Fetch API. The options argument allows the caller to customize the behavior of the API call as needed, for example by setting custom headers.

As introduced earlier, the Next.js fetch() function also accepts special options for handling the caching behavior. The optionsparameter can also be used to change that as in the following example:

// retrieve the articles from the CMS // by ignoring the data in the cacheconst articles = await CmsAPI.getArticles( { limit: 20, offset: 0, }, { cache: "no-store", }); Since <externalService>API.js files may contain server-only logic—such as basic authentication information—you do not want developers to accidentally use them in React client components.

As a rule of thumb, server components should retrieve data using the Next.js API layer, while client components should communicate with external APIs with an HTTP client like Axios or using a React API layer. Although the Next.js and React API layers serve a similar purpose, they consist of files that have different scopes.

To prevent developers from using the Next.js API layer in client-side React components, install the server-only package. Add it to the project dependencies with the following command:

npm install server-only Then, mark your <externalService>API.js files as server-only with this line at the top of the files:

import "server-only";// API layer file definition ... Now, if a developer imports a <externalService>API.js into a client component, the build process of the Next.js application will fail with the error below:

You're importing a component that needs server-only. That only works in a Server Component which is not supported in the pages/ directory. Read more: https://nextjs.org/docs/getting-started/ Note that here you saw how to implement a single <externalService>API.js file, but your Next.js API layer can contain many of them.

API layer in actionTime to see the API layer in action in a Next.js demo application that relies on the free and open-to-use PokeAPI project as an external API service provider.

Clone the GitHub repository of the demo project, install the dependencies, and launch the demo site locally with the following commands:

git clone https://github.com/Tonel/api-layer-nextjscd api-layer-nextjsnpm installnpm run dev Visit the https://localhost:3000 page in the browser and you will see:

The data presented on this page comes from the GET https://pokeapi.co/api/v2/pokemon?limit=21&offset=0 endpoint.

You can verify that the Next.js fetch() function caches data by default as expected by inspecting the cache folder inside the

.next build directory:

Open the file inside the fetch-cache subfolder and you will see that it contains a file with hashed data related to the https://pokeapi.co/api/v2/pokemon?limit=21&offset=0 endpoint.

Click on a Pokemon card to reach the detail card page:

This will trigger a new server-side API call and lead to a new cache file in the .next/cache/fetch-cache folder.

Et voilà! You just learned how to build an API layer in Next.js.

ConclusionIn this article, you understood what a Next.js API layer is, the main benefits it brings to your architecture, and how to implement it using fetch(). An API layer is a set of files that provides everything your server components need to retrieve data via API calls. It centralizes the server-side API logic in your project and makes your codebase easier to maintain. Adding this layer to your Next.js App Router application is easy and does not require external dependencies. You have now learned how to implement a server-side API layer in Next.js and saw it in action through a simple demo.

The post Why Your Next.js Site Needs an API Layer and How To Build It appeared first on Semaphore.

View Details

React 18 has been around for more than two years, and it is finally time to welcome React 19. The main innovation introduced, the one we all love is the React Compiler! It promises to forever simplify frontend development by eliminating the need for manual memoization optimizations.

In this guide, you will understand what React Compiler is, how it works, what benefits it introduces, and how to prepare your application.

What React 19 Brings To the TableReact 19, to be released at React Conf 2024 on May 15-16, 2024, is the long-awaited next version of this framework. React 18 was released in 2022, and since then, web technologies have evolved greatly. It is high time for an update.

Not only will version 19 be a step forward but it promises to forever change the way developers build applications in React. Some of the most exciting features React 19 plans to introduce are:

  • Server components: Server-side rendering of components for faster page loading and better SEO. By processing components on the server before delivering the page to users, React 19 enables faster website loading times, improved search engine visibility, and smoother data management. Next.js already uses this feature.
  • Actions: Streamline the management of data and interactions within web pages. Actions make it easier to update page information through forms, removing complexities and simplifying the user experience.
  • Optimized asset loading: Background loading of site assets for smoother page changes. React 19 can begin loading images and other files in the background while users are still browsing the current page, reducing wait times during page transitions.
  • Document metadata: Streamlined SEO management thanks to the new <DocumentHead> component. Adding titles and meta tags to pages will be more intuitive, improving search engine optimization without the need for repetitive coding.
  • Web components: Improved compatibility with the Web Components standard for more flexible and compatible frontend development.
  • Enhanced hooks: Greater control over the lifecycle and state of components through both existing and new hooks. The end goal is to simplify the coding process, making React development more efficient and enjoyable.
  • React compiler: Automatic conversion of React code into standardized, optimized JavaScript code. It allows compiled React code to automatically render only the right parts of the UI when the state changes, reducing the need for useMemo, useCallback, and memo. This means faster React applications with simplified codebases.

All of these features are great, sure, but what stands out is React Compiler. This upgrade promises to change React development forever. Let’s now try to understand the why behind it and what benefits it will introduce!

React Compiler: Everything You Need to KnowDig into the React 19 Compiler tool, exploring what it is and how it works.

React’s Core Mental ModelTo understand the why behind React Compiler, you first need to delve into some key React concepts.

At the heart of React is a declarative and component-based mental model. In frontend development, declarative programminginvolves describing the desired end state of the UI without specifying each step to get there through DOM manipulation. Meanwhile, the component-based approach breaks down UI elements into reusable, concise, self-contained building blocks, promoting modularization and ease of maintenance.

To efficiently identify specific DOM elements that require updates, React employs an in-memory representation of the user interface called the virtual DOM. In case of changes to the application state, React compares the virtual DOM with the real DOM, identifies the minimum set of changes needed, and accurately updates the real DOM.

In short, the mental model is that React re-renders whenever the state of the application changes. However, sometimes React can be too reactive, resulting in unnecessary re-renders that significantly slow down your application.

Re-Rendering Hell: The Need for a CompilerReact’s agility in responding to changes in application state is a double-edged sword. On the one hand, it simplifies frontend development due to its declarative approach. On the other, it can lead to excessive re-rendering of the components in the UI in response of state changes.

Re-rendering problems are particularly common when dealing with JavaScript data structures such as objects and arrays. The problem is that there is no computationally efficient way to compare two objects and arrays to see if they are equivalent (have the same keys and values) in JavaScript.

Consider the following scenario. You have a React component that generates a new object or array upon each render, as in the example below:

import React from "react";const AlphabetList = () => { // define the alphabet array const alphabet = Array.from({ length: 26 }, (_, i) => String.fromCharCode(65 + i)); return ( <div> <h2>Alphabet List</h2> <ul> {/* render the alphabet as list items */} {alphabet.map((letter, index) => ( <li key={index}>{letter}</li> ))} </ul> </div> );};export default AlphabetList; Although the content of the local array will be the same at each rendering, React cannot efficiently know that. As a result, it may trigger a re-render in the nested DOM elements of the component relying on the values in that array, unaware that the UI should remain identical. This re-rendering mechanism can quickly spiral out of control, significantly impacting application performance and user experience.

To optimize re-rendering behavior and avoid those issues, React developers must manually introduce memoization into their components. In React, memoization involves caching the results of expensive computations or component outputs based on their input parameters. By storing and reusing these results, memoization helps prevent unnecessary re-reading of components, improving the overall efficiency and performance of a React application.

React 18 provides several memoization tools:

  • React.memo(): A higher-order function to avoid re-rendering a component when its props remain unchanged.
  • useMemo(): A React hook that caches the result of a calculation between re-renders, reducing redundant computations.
  • useCallback(): A hook from React that caches the definition of a function between re-renders, avoiding unnecessary recreations of functions. Learn more in our guide to the React useCallback() hook.

Thanks to the useMemo() hook, you can optimize the <AlphabetList> component to avoid unnecessary re-renders as follows:

import React, { useMemo } from "react";const AlphabetList = () => { // define the alphabet array via useMemo() const alphabet = useMemo(() => { return Array.from({ length: 26 }, (_, i) => String.fromCharCode(65 + i)); // no dependencies, so it will only be calculated once on the first render }, []); return ( <div> <h2>Alphabet List</h2> <ul> {/* render the alphabet as list items */} {alphabet.map((letter, index) => ( <li key={index}>{letter}</li> ))} </ul> </div> );};export default AlphabetList; The memoization tools offered by React are certainly powerful. At the same time, their introduction is nothing more than a clear departure from the declarative philosophy underlying React’s core mental model. This is because the burden falls on developers, who must not only describe the final state of the UI but also explicitly manage rendering optimizations. Manual memoization also introduces code complexity and maintenance headaches.

The solution? An advanced compiler capable of converting React code into optimized JavaScript code, so that components are automatically re-rendered only in case of significant changes in state values.

What Is React Compiler?React Compiler, also known as React Forget, is an optimizing compiler for React. It now powers Instagram’s web portal in production and will be deployed on other Meta applications before its first open-source release.

The original goal of the compiler was to enforce React’s core programming model by automatically generating the equivalent of memo, useMemo, and useCallback calls to minimize the cost of re-rendering. The project has evolved greatly from its beginnings, moving from an “auto-memoizing compiler” to an “automatic reactivity compiler.”

The real objective of React Forget is now to ensure that React applications have the right amount of reactivity by default. In other words, apps should re-render only when state values meaningfully change. Currently, React re-renders a component when object identity changes. With React Forget, it will re-render only when the semantic value of an object changes — but without incurring the runtime cost of deep comparisons.

From an implementation point of view, React Compiler applies automatic memoization. However, the team behind it considers reactivity framing as a more complete way to understand what it does. If you want to learn more about the inner workings and logic of the React compiler, read the React Labs section dedicated to it.

See React Forget in action in the video below: https://www.youtube.com/watch?v=qOQClO3g8-Y&

While JavaScript is a notoriously difficult language to optimize because of its dynamic nature and loose rules, React Compiler can compile code safely by modeling both JavaScript rules and the “rules of React.” These rules limit what developers can do, helping to carve out a safe space for the compiler to perform its optimizations.

The Rules of ReactReact comes with a set of rules that are intended to support high-quality web applications. Developers should follow these rules, which also represent what React Compiler is based on.

Some of the most important rules of React are:

  • Components must be idempotent: React components should always produce the same output given the same set of inputs, which include props, state, and context.
  • Side effects must run outside of render: Side effects, such as data fetching, subscriptions, or manually changing the DOM, should not be performed during the rendering process of a component. Instead, they should be executed in lifecycle hooks like useEffect.
  • Props and state are immutable: Props and state in React components should be treated as immutable, meaning they should not be directly modified. Changing props or state directly can lead to bugs and unpredictable behavior.
  • Return values and arguments to hooks are immutable: Once values are passed to a React hook, they should not be modified. Hooks rely on the immutability of their arguments and return values to maintain consistency and predictability in component behavior.
  • Values are immutable after being passed to JSX: Do not mutate values used in JSX after this has been rendered. Any necessary mutations should be performed before the JSX is created to ensure that the rendered output remains consistent.
  • Never call component functions directly: React components should only be used within JSX and not called directly as regular functions.
  • Never pass around hooks as regular values: React hooks, such as useState and useEffect, should only be called inside functional components. Using them as regular values can lead to unexpected behavior and violate the rules of hooks.
  • Only call hooks at the top level: React hooks should always be called at the top level of functional components, before any conditional statements or loops. This ensures that hooks are called in the same order on every render and maintain their intended behavior.
  • Only call hooks from React functions: Hooks should only be called from within React function components or custom hooks. Calling hooks from regular JavaScript functions can lead to errors and violate the rules of hooks.

Enable Strict Mode and configure React’s ESLint plugin to make sure that your React application follows these rules.

Benefits and HopesThe main benefits introduced by React Compiler are:

  • No more memoization headaches: Developers no longer have to manually implement and manage memoization strategies in their code. This reduces complexity and the likelihood of errors, streamlining the development process.
  • Better developer experience: Developers can focus more on building features and less on performance optimization, leading to increased productivity and satisfaction. They will finally be able to fully embrace the React declarative approach.
  • Faster React applications: Render components only when necessary, minimizing unnecessary computations and overhead. This results in faster and more responsive user interfaces, improving overall performance and user experience.

These are all promising changes, but we have yet to see what effect this new tool will have on code development. To ensure that the compiler does its job, your code must follow the rules of React. Here is why the official team recommends adopting ESLint and similar tools to prepare your application for React Compiler.

React Compiler: A Frontend Revolution?It is difficult to say soon whether the React Compiler will be enough to spark a real revolution in the realm of frontend development. What is for sure is that it has all the credentials to significantly change the development of future React applications. By introducing automatic memoization, this compiler can automatically speed up React applications and improve the developer experience. These are just some of the benefits this promising ambitious project brings to the tabl.

React Conf 2024 is just around the corner, and we look forward to seeing what breakthroughs this ambitious project will have on the frontend world!

The post React Compiler: What Is It and How Will It Change Frontend Development? appeared first on Semaphore.

View Details

A crucial step in the software creation process is effective testing. It ensures the programs’ general quality, security, and usefulness. Testing teams do, however, have a number of difficulties, one of which is finding faulty tests. Sometimes these tests fail for no obvious reason at all, which confuses people and impedes the progress of development. These tests are called flaky tests. This article covers various testing tools that can help you spot, control, and minimize the effect of flaky tests.

Challenges in Identifying Flaky TestsIn software testing, random test failures may be very frustrating. Flaky tests are unexpected since they can pass on one try but fail on another. These inconsistencies might be caused by difficult-to-replicate external dependencies, complex links between many code parts, or timing problems. Finding incorrect tests becomes more difficult when several tests are run simultaneously, application behavior changes, and unexpected test execution conditions occur.

The asynchronous and dynamic loading of web application components makes the job more difficult. As a result, it is difficult to create a reliable and consistent testing environment and find the root cause of random test failures. This is when certain testing tools become important. By resolving these problems, you may improve the dependability of your test results using these tools. They allow testing teams to identify, control, and mitigate the effect of irregular tests on the overall testing strategy. You can read more about the best practices to help mitigate it here

Tools for Detecting and Managing Flaky TestsSeveral testing tools can simplify the process of finding and handling flaky tests. Let’s explore some famous options:

TestNGTestNG, a famous Java testing framework, includes built-in features for handling flaky tests. It uses listeners and analyzers to closely watch test runs, discover problems, and properly group tests.

  • Listeners: These are important for recording events during a test run. They offer hooks for running custom code before or after defined test events. Listeners allow you to create custom rules for identifying and handling unreliable tests.
  • Analyzers: TestNG analyzers have a built-in way for rerunning failed tests, which helps decrease flakiness. The analyzer checks the original test results, finds failures, and selectively retries the failed tests based on predefined criteria.

Adding TestNG to your project

Here is a step-by-step guide on installing TestNG in IntelliJ IDEA (it comes bundled with TestNG 7.1.0)

  • For Maven users:
  • Navigate to your project’s root directory.
  • In pom.xml, hit Alt Insert and choose Dependency.
  • In the window that appears, enter testNG into the search area.
  • Locate the org.testng:testing dependency, choose its version from the search results, and then click Add.
  • After adding the dependency to pom.xml, import the modifications by pressing Ctrl Shift + O or clicking the refresh icon to import the changes.
  • For Gradle users:
  • Open the build.gradle in your project’s root location.
  • Press Alt Insert in build.gradle and choose Add Maven artifact dependent.
  • Enter testNG into the search area of the window that opens.
  • Find the requirement for org.testng:testing, choose its version from the search results, and click Add.
  • Once the dependent has been added to build.gradle, import the changes by pressing Ctrl Shift O or refresh icon to import changes
  • For IntelliJ build tool:
  • Select File then Project Structure from the main menu or use Ctrl Alt Shift S
  • Under Project Settings, choose Libraries, and then click the New Project Library from Maven option.
  • In the dialog that appears, specify the required library asset, for example: org.testng:testng:6.14.3.
  • Apply your changes and close the dialog.

Creating a new TestNG class

The simplest way to make a new test class in IntelliJ IDEA is to use a special intention action that can be called from the source code. In this case, the IDE makes a new test class and produces test code for it, the package, or function. Here are steps to take:

  • Place the caret at the class for which you wish to build a test in your production code in the editor, hit Alt Enter, and choose build Test.

  • Choose the library you wish to use from the Create Test dialog box. It will ask you to download the required library if you don’t already have it. Click Fix to accomplish it.

  • The IDE will add the missing dependencies to your pom.xml if you’re using Maven. Manually add the required dependencies for projects using Gradle.

  • Set the name and location of the test class and choose the methods you wish to test. Press OK.
  • Consequently, a new test class with the given name and produced test methods are created under the Test Sources Root by IntelliJ IDEA.

Running TestNG tests Executing TestNG tests within IntelliJ IDEA is straightforward:

  • Individual Test: Click the “Run” icon in the gutter next to the desired test.
  • Entire Test Class: Right-click the test class name and select “Run” -> “Run.” The “Run” tool window will display the test results for your evaluation.

JUnit Flaky Test PluginThe JUnit Flaky Test Plugin is an extremely useful tool for developers working with JUnit test packages. It handles a prevalent problem which is flaky tests. These are tests that give inconsistent results, with some passing and others failing for no obvious reason. This difference can hinder development progress by wasting time studying mistakes that do not suggest true issues. JUnit 5 is the latest version of the JUnit testing system, offering a contemporary base for developer-side testing on the Java Virtual Machine (JVM). This includes emphasis on Java 8 and higher, as well as allowing for a range of testing methods. Here is how to plan and perform tests.

Creating Project

  1. Navigate to file, select new and project from the main menu.
  2. Choose Java from the left selection in the New Project wizard.
  3. Give the project a name, such as `junit-tutorial , and choose Maven, Gradle or IntelliJ as the build tool.
  4. Choose the JDK that you wish to utilize for your project from the list of JDKs.
  5. Choose Add JDK and enter the path to the JDK home directory if the JDK is installed on your machine but is not defined in the IDE.
  6. Click Download JDK if your machine is missing the required JDK.
  7. Press the Create button.

Add dependency

We must add JUnit as a dependency in order for our project to leverage JUnit functionalities.

  • For Maven users:
    • In your project’s root directory, open pom.xml.
    • Press Alt Insert in pom.xml and choose Dependency.
    • Enter org.junit.jupiter:junit-jupiter in the search box of the dialog that appears.
    • In the search results, choose the required dependency and select Add.
    • Once the dependency has been added to pom.xml, import the modifications by pressing Ctrl Shift O or selecting refresh to import changes.
  • For Gradle users:
    • Launch the build.gradle in your project’s root directory. Ctrl Shift 0 may be used to swiftly go to a file by entering its name.
    • Press Alt Insert in build.gradle and choose Add Maven artifact dependency.
    • Enter org.junit.jupiter:junit-jupiter in the search field of the tool window that appears.
    • In the search results, choose the required dependency and select Add.
    • Once the dependent has been added to build.gradle, import the modifications by pressing Ctrl Shift O or clicking refresh to import changes
  • For IntelliJ build tool:
    • Select File, Project Structure (Ctrl Alt Shift S) from the main menu.
    • Choose Libraries from the Project Settings menu, then click the New Project Library from Maven button.
    • In the resulting dialog box, choose the required library artifact, such as: org.junit.jupiter:junit-jupiter:5.9.1.
    • After making your adjustments, close the dialog box.

Creating Sample Code and Tests

In the Project tool window go to src/main/java and create a Java file called Calculator.java.

import java.util.stream.DoubleStream;public class Calculator { static double add(double... operands) { return DoubleStream.of(operands) .sum(); } static double multiply(double... operands) { return DoubleStream.of(operands) .reduce(1, (a, b) -> a * b); }} Right-click on “Calculator” in the Project tool window and select “Create Test.” Choose the two methods you want to test (add and multiply).

The editor takes you to the newly created test class. Modify the add() test as follows:

@Test@DisplayName("Add two numbers")void add() { assertEquals(4, Calculator.add(2, 2));} This short test will determine whether our algorithm adds two and two correctly. The test has a more practical and useful name thanks to the @DisplayName annotation. What would happen if you wanted to include more than one statement in a single test and run it all even if some of the assertions didn’t pass? The assertAll() function accepts a list of lambda expression assertions and checks that each one has been verified. Seeing a specific result instead of the test’s overall result is always more handy than having several single assertions.

Run tests

After we have set up the code for the testing, we can run the tests and find out if the tested methods are working correctly. Running individual test, click run in the gutter. To execute all tests in a test class, click execute against the test class declaration and then choose Run. The final result can be seen below:

RSpec’s –bisect OptionA potent method called –bisect is given by the well-known Ruby testing framework RSpec to overcome inconsistent test failures. When tests only fail after finishing particular tests before them, a situation that might be difficult to fix using standard methods, this flag becomes extremely important.

A repeating method is invoked by the --bisect option to find the source of test flakiness. It starts by running your whole test suite, dividing it in half carefully and running each half separately. Subdivision continues until the smallest set of tests successfully reproduces the mistake in the half that still shows the failure.

Practical Example Consider a Ruby test suite with ten files (spec/calculator_1_spec.rb to spec/calculator_10_spec.rb), and one of them occasionally fails. You assume the failure is caused by the execution order, but you can’t determine the specific combination.

Using --bisect:

  1. Open your terminal and navigate to your Ruby project directory.
  2. Run the following command to start the bisect process:

rspec --seed 1234 --bisect --seed 1234: This guarantees that tests are executed in a consistent sequence during the bisect process. RSpec will provide findings that include rounds of bisection, eventually narrowing down the failed samples. Here is a sample output:

Round 1: bisecting over non-failing examples 1-9 .. ignoring examples 6-9Round 2: bisecting over non-failing examples 1-5 .. ignoring examples 4-5Round 3: bisecting over non-failing examples 1-3 .. ignoring example 3Round 4: bisecting over non-failing examples 1-2 .. ignoring example 1Bisect complete! Reduced necessary non-failing examples from 9 to 1. Success! RSpec has identified the minimal set causing the failure. The final output provides a minimal reproduction command that includes only the necessary failing specs. This command below allows you to re-run the failing tests in isolation, making it easier to diagnose the root cause.

rspec ./spec/calculator_10_spec.rb [1:1] ./spec/calculator_1_spec.rb [1:1] --seed 1234 You can use Ctrl-C to end the bisect operation at any time. You can test it with projects here

Visual Regression Testing ToolsVisual Regression Testing (VRT) tools are crucial in detecting even the slightest visual differences that could be overlooked during conventional testing. In this chapter, we will review Percy and Applitools, two prominent players in the VRT industry. Both platforms provide unique benefits that are worth exploring.

Overview of Percy’s Visual Testing Capabilities

The purpose of Percy, an automated visual testing tool, is to identify visual inconsistencies in online applications. It helps teams ensure the visual excellence of their user interface by capturing and comparing screenshots of the application’s visual state during testing. Jest, Cypress, and Selenium are just a few of the well-known testing frameworks that Percy easily interfaces with. Additionally, it works with CI/CD systems like GitHub Actions and Jenkins. It creates baseline images of the user interface for your programme and notifies you of any differences by comparing subsequent test runs with these baselines.

Integrating Percy with Jest:

  • Begin by installing the Percy package in your project.

npm install --save-dev @percy/puppeteer * Set up Percy in your Jest configuration.

// jest.config.jsmodule.exports = { // ... other Jest configurations setupFilesAfterEnv: ['<rootDir>/percy.setup.js'],}; * Create a percy.setup.js file with the following content:

// percy.setup.jsconst { percySnapshot } = require('@percy/puppeteer');// Ensure Percy is runningbeforeAll(async () => { await page.goto('http://localhost:5338');});// Take snapshots using Percytest('Snapshot', async () => { await percySnapshot(page, 'Snapshot');}); * Run Jest with Percy: Execute your Jest tests with Percy.

npx percy exec -- jest The integration with Percy enhances your visual testing strategy, providing a comprehensive view of your application’s UI changes.

Integration with Selenium:

If you’re working with Java and Selenium, you can use the percy-java-selenium library in your project. Add it to your Maven project dependencies

<dependency> <groupId>io.percy</groupId> <artifactId>percy-java-selenium</artifactId> <version>1.3.0</version></dependency> A sample of test code in Java for taking snapshot:

import org.openqa.selenium.WebDriver;import org.openqa.selenium.chrome.ChromeDriver;import io.percy.selenium.Percy;public class Example { public static void main(String[] args) { // Create a new WebDriver instance using ChromeDriver WebDriver driver = new ChromeDriver(); // Navigate to the specified URL (in this case, "https://example.com") driver.get("https://example.com"); // Initialize Percy with the WebDriver instance Percy percy = new Percy(driver); // Take a snapshot of the current page with a descriptive name percy.snapshot("Java example"); }} In short, this code sets up Percy, starts “https://example.com“, sets up a Chrome WebDriver, and takes a picture for testing and comparison. Percy will keep this sample for future use as a guide and for checking for bugs. Keep in mind that you should use the real URL of the site you want to test instead of “https://example.com.” You can test it with various Java projects here

Applitools Eyes: Visual AI Testing

One of the most complete Visual AI testing tools is Applitools Eyes, which has a lot of options for finding UI issues. Users pay based on how many tests they run and the services they need, using a commercial price plan. Test scripts for Applitools Eyes can be written in Python, Java, and JavaScript, among other languages. The Applitools SDK is used to compare snapshot that were taken while tests were running. In particular, Applitools Eyes cuts down on false results by using AI and machine learning. It ensures accurate test results by instantly finding and disregarding changing visual aspects.

To start using Applitools Eyes for Visual AI Testing, follow these steps:

  • Sign Up for an Account: Visit the Applitools sign-up page to create a new account.
  • Login to Applitools Dashboard: Once registered, log in to the Applitools dashboard.
  • Set Up the SDK in Your Project: To start using Applitools, you’ll need to install and configure the Applitools SDK. The SDK is available for popular programming languages and frameworks and can be easily integrated to existing automation test frameworks. Each Applitools account has a secret API key. Visual tets require the API key to authenticate the account and upload test results to the Applitools cloud and connect the results to that account
  • Write Your First Visual Test: Using Applitools Eyes, create a test script that defines visual checkpoints. This entails taking screenshots of your application and confirming their accuracy.

Example (Java with Selenium):

import com.applitools.eyes.selenium.Eyes;import org.openqa.selenium.WebDriver;import org.openqa.selenium.chrome.ChromeDriver;public class YourVisualTest { public static void main(String[] args) { // Initialize the WebDriver for Chrome browser (you can modify this to use a different browser) WebDriver driver = new ChromeDriver(); // Create an Eyes object to perform visual testing Eyes eyes = new Eyes(); // Set your Applitools API key to enable communication with the platform (replace with your actual key) eyes.setApiKey("Your_Applitools_API_Key"); try { // Start the visual test by providing the application name and test name of your choice eyes.open(driver, "Your App Name", "Your Test Name"); // Navigate to the URL of your web application under test driver.get("Your App URL"); // Capture a visual checkpoint of the entire page. // Consider using more specific selectors for targeted testing in the future eyes.checkWindow("Full Page"); } finally { // Always close the WebDriver to avoid resource leaks driver.quit(); // If the test was aborted due to an exception, abort the Eyes test as well eyes.abortIfNotClosed(); } }} * Run Your Visual Test: When you run your test script, Applitools will take screenshots of your application and compare them to a baseline. * Review Results in the Dashboard: Return to the Applitools dashboard to see the visual tests’ outcomes. It will draw attention to any variations between your application’s baseline and current states. Update the baseline in the Applitools dashboard to reflect the new intended visual design as your application develops.

Adding visual testing to your workflow guarantees a visually consistent and enjoyable user experience, regardless of whether you want the powerful AI-driven analysis offered by Applitools or the seamless integration, collaboration, and ease of use offered by Percy.

ConclusionThis article discusses the challenges faced by flaky tests in software development, with a particular emphasis on the role of testing tools in resolving these issues. To study more of this tools and practical experiences in large-scale commercial settings and investigating technologies such as TestNG, JUnit, and Percy, check out this in-depth study. It becomes clear that the testing tools used have a substantial impact on test suite dependability. The study emphasizes the ongoing need for research and innovation in testing technologies to improve their ability to detect and manage flaky tests. As the software development community grows, a concentrated effort to refine and advance testing tools is critical for establishing confidence, assuring reliability, and successfully negotiating the complexities of flaky testing scenarios.

The post Testing Tools to Identify Flaky Tests appeared first on Semaphore.

View Details

Forget fancy titles and complex frameworks. The secret to an organization’s success might lie in a surprisingly simple act: two people moving a couch. This seemingly mundane task, according to Gene Kim, holds profound lessons about communication, problem-solving, and creating a frictionless work environment.

Edited transcriptionFor over 25 years, author and researcher Gene Kim has studied the intricate dynamics of high-performing technology organizations, including technical practices, architectural approaches, and cultural norms that allow companies to operate at their peak. Gene’s latest book, Wiring the Winning Organization co-authored with Dr. Steven Spear, collects these rich learnings into a framework for optimizing how teams interact and enable their workflow.

The Couch Metaphor Explained: Illuminating organizational principles, team coherence, and DevOpsAt the core of Wiring the Winning Organization lies a deceptively simple yet profound metaphor “that I think is probably one that has changed the way I view the world,” Gene says. The couch metaphor involves two individuals, Steve and Gene, deciding how to move a couch. The act of two people moving a couch may seem, at first, devoid of deeper significance and complexity, being, apparently, just brute manual labor. However, multiple layers of essential problem-solving skills are potentially required to complete this rudimentary task. As Gene explains, ” They have to solve a lot of problems, like where’s the center of gravity, to get through a narrow doorway around which axis they rotate, to get through a narrow winding set of stairs, who goes first?

As such, the couch metaphor chiefly illustrates teams’ necessity to navigate obstacles: rapid experimentation, trial-and-error, fast feedback loops, communication, and coordination. As Gene describes, “You don’t need consultants, you don’t need focus groups. Just by picking up the couch—fast feedback, experimentation, communication, coordination—they’re going to figure out how to solve the problem.”

Likewise, the metaphor also shows how artificial constraints can severely disrupt this coherence between actors. What’s more, such disruption is often imposed by leadership, says Gene, “as leaders do things that make their work more difficult: We can turn off all the lights—it takes longer, more dangerous, can damage the couch themselves—, we introduce a lot of background noise, or put in an intermediary between Steve and Gene so they can’t talk to each other directly.” At its core, the couch analogy underscores leaders’ responsibility to figure out the workflow. Rather, “their job is to set up the architecture so everyone has what they need, when they need it, in the right format and time,” Gene states. “The opposite is that important failure signals are suppressed or extinguished, leading to catastrophic outcomes,” he adds.

A key part of understanding this architecture, as outlined in the book, is recognizing three distinct layers: the object being worked on (like code), the tools and instruments used (IDEs, etc.), and crucially, the overarching organizational wiring that dictates how components interact. “In any good to great transformation, the only thing that changes is layer three—the management system, the wiring,” Gene explains. Even though it might be the “same people, same technology, same equipment”, the wiring is what enables or obstructs high performance.

In the software development domain, the couch metaphor can also illustrate the role of DevOps. The “ever-increasing number of functional specialties” such as dev, QA, security, and operations has “partitioned the couch so they could not effectively communicate and coordinate.” To solve this issue, DevOps, through practices like CI/CD and automated deployments, provides a “different wiring” that favors cross-functional collaboration, helping “to better couple those components together and decouple them.”

Besides, as new domains like MLOps and AI Ops are integrated, Gene sees immense value in established frameworks: “It’s about creating governance processes to make sure we don’t get the company into trouble…It’s just a great time because it’s this community being handed the most important responsibilities.”

Embracing functional specialtiesWhile some may wish for a retreat from increasing specialization, Gene is confident that “functional specialties are not going to go away.” Functional divides are inescapable for solving complex problems and despite it being impossible for a single individual to be proficient in all fields, specialization is what delivers the greatest results.

The solution, therefore, isn’t eliminating specialties but rather optimizing how they integrate. Behind this integration, says Gene, is “the functional owner,” who is “the product owner of the thing you are integrating their work into.” By separating responsibilities around methodology from high-level objectives, specialized groups can maintain autonomy while still operating coherently.

The bottom lineWant to delve deeper into Gene’s work? Here are some starting points:

  • You can find more information about Gene Kim, his books, and research on his official website
  • Aside from Wiring the Winning Organization, his books include:
    • The Phoenix Project: A novel about IT, DevOps, and Helping Businesses Succeed. Co-authored with Kevin Behr and George Spafford, The Phoenix Project follows an IT manager as he takes over the struggling operations team at Parts Unlimited and attempts an audacious turnaround.
    • The Unicorn Project: A follow-up to The Phoenix Project, focusing on the development side of IT
    • The DevOps Handbook: Co-authored with Patrick Debois, John Willis, and Jez Humble, providing insights into creating world-class agility, reliability, and security in technology organizations.
  • You can follow Gene Kim on his X account @geneskim.

The post Gene Kim on Unpacking Organizational Wiring appeared first on Semaphore.

View Details

In the context of machine learning (ML), flaky tests can be troublesome as they introduce uncertainty into the evaluation process, potentially leading to incorrect conclusions about model performance or behavior.

In this article, we’ll:

  • Introduce what flaky tests are.
  • Discuss issues caused by flaky tests in ML.
  • Consider and Evaluate flakiness in ML.
  • Discuss strategies to avoid flaky tests in ML.

Introduction to Flaky TestsFlaky tests refer to automated tests that produce non-deterministic results, meaning they may pass or fail inconsistently under the same conditions, without any changes to the code. In other words, they’re unpredictable and can pass or fail when re-run, even if there have been no modifications to the code base, dependencies, or test environment.

In the context of ML, flaky tests can manifest in various forms, causing issues that include, among others:

  • Unstable model performance metrics.
  • Inconsistent validation results.
  • Unpredictable behavior during training or inference.

Issues Caused by Flaky Tests in Machine LearningDue to their nature, flaky tests introduce two different sets of issues in the context of machine learning:

  • Infrastructure-related issues.
  • Model-related issues.

Let’s discuss both of them.

Infrastructure-related IssuesIn ML environments, flaky tests can introduce several significant issues on the side of the infrastructure, complicating the development, deployment, and maintenance of ML models.

We can recall the following:

  1. Resources drain. Investigating flaky tests requires significant time and computational resources. Teams often need to run tests multiple times to determine whether a failure is due to a real bug or just flakiness. This process can consume computational resources, especially with complex ML models that require substantial time and hardware to train and evaluate.
  2. Development slowdown. Flaky tests can significantly slow down the development cycle. Every time a test fails inconsistently, developers and data professionals might need to interrupt their current work to investigate whether the failure indicates a real issue due to the code or model. This disruption can delay the introduction of new features or the resolution of existing bugs.
  3. Impact on continuous integration/continuous deployment (CI/CD) pipelines. In a CI/CD environment, automated tests trigger actions such as building, testing, and deploying applications. Flaky tests can cause interruptions in the pipelines, requiring manual intervention to proceed. This defeats the purpose of automation, leading to inefficiencies and potential delays in deployment schedules. In addition, re-running pipelines adds to your monthly bill.
  4. Reduced trust in testing frameworks. Consistent reliability in test results is crucial for the development team’s confidence in their testing framework. Flaky tests undermine this trust, making it challenging to distinguish between genuine failures that need attention and random failures that can be ignored. Over time, this can lead to a scenario where test failures are dismissed without investigation, potentially allowing real defects to slip through.

Model-related issuesFlaky tests can significantly impact various aspects of the machine learning model lifecycle, including training, validation, and generalization. These impacts can compromise the model’s performance, its reliability in production environments, and ultimately the project’s success.

We can recall the following:

  1. Unreliable model evaluation. Flaky tests can lead to unreliable assessments of model performance, making it challenging to accurately determine the effectiveness of ML algorithms accurately.
  2. Inconsistent validation. Flakiness in validation procedures can obscure the true generalization capabilities of ML models, potentially leading to overfitting or underfitting. This inconsistency can emerge from tests that depend too much on specific data subsets or initialization parameters that vary across runs.
  3. Reduced confidence in results. Flaky tests can lead to both underestimation and overestimation of model performance on validation datasets. This makes it challenging to make informed decisions about whether a model is ready for deployment, also contrasting decision-making processes based on model outputs.
  4. Difficulty in hyperparameter tuning. The process of hyperparameter tuning relies on consistent feedback to guide the search for optimal model configurations. Flaky tests introduce noise into this feedback, potentially leading to suboptimal tuning decisions based on misleading test outcomes.
  5. Compromised generalization ability. The ultimate goal of an ML model is to perform well on unseen data. Flaky tests, by obscuring the true performance of a model on validation sets, can lead to overconfidence in its generalization ability. This might result in deploying models that perform poorly in real-world scenarios.

Examples of Flakiness in Machine LearningIn this section, I want to consider flakiness due to ML models themself. In other words, in machine learning, flakiness also exists due to the statistical nature of the models.

In the context of machine learning, in fact, flaky tests can arise due to various reasons such as:

  • Non-deterministic algorithms.
  • Random initialization.
  • Data shuffling.

Let’s see an overview of some Python examples demonstrating common scenarios where flakiness might occur in ML.

Example 1: Flakiness Due to Random InitializationLet’s consider the following code:

import numpy as npfrom sklearn.datasets import make_classificationfrom sklearn.model_selection import train_test_splitfrom sklearn.ensemble import RandomForestClassifier# Generate synthetic dataX, y = make_classification(n_samples=1000, n_features=20, random_state=42)# Split data into train and test setsX_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)# Train Random Forest classifierclf = RandomForestClassifier(n_estimators=100, random_state=np.random.randint(100))clf.fit(X_train, y_train)# Evaluate classifieraccuracy = clf.score(X_test, y_test)print("Accuracy:", accuracy) In this example, the accuracy of the Random Forest classifier varies across runs due to the random_state parameter used for initialization.

In this case, setting random_state=42 in the make_classification() function results in an accuracy of 0.895 approximately.

Instead, setting random_state=0 make_classification() results in an accuracy of 0.945 approximately.

To verify the different results that the modification of this parameter leads to, good tests involve:

  • Trying different values in the make_classification() (which generates the classification data) and train_test_split()(which splits the generated data into the train and test set) functions.
  • Not specifying the value of the parameter. This way, each time you run the model, you get different values because the values of the random_state parameter are set randomly.

Example 2: Flakiness Due to Data ShufflingLet’s consider the following scenario:

from sklearn.datasets import load_irisfrom sklearn.model_selection import KFoldfrom sklearn.svm import SVC# Load iris datasetiris = load_iris()X, y = iris.data, iris.target# Initialize SVM classifierclf = SVC(kernel='linear', random_state=42)# K-fold cross-validationkf = KFold(n_splits=5, shuffle=True, random_state=42)accuracies = []for train_index, test_index in kf.split(X): X_train, X_test = X[train_index], X[test_index] y_train, y_test = y[train_index], y[test_index] clf.fit(X_train, y_train) accuracy = clf.score(X_test, y_test) accuracies.append(accuracy)mean_accuracy = np.mean(accuracies)print("Mean Accuracy:", mean_accuracy)print(accuracies) This results in:

Mean Accuracy: 0.9733333333333334[1.0, 1.0, 0.9666666666666667, 0.9333333333333333, 0.9666666666666667] In this example, the K-fold cross-validation produces different accuracy values across runs due to the shuffling of data during each split, leading to flakiness in test results.

Also, in such cases, every time we run the code, the value of accuracy we get may be different because of the shuffle=Trueparameter in the KFold() function.

Example 3: Flakiness Due to Algorithm VariabilityLet’s consider the following:

from sklearn.datasets import load_irisfrom sklearn.ensemble import RandomForestClassifierfrom sklearn.model_selection import train_test_split# Load Iris datasetiris = load_iris()X, y = iris.data, iris.target# Introduce flakiness due to randomness in bootstrappingaccuracies = []for _ in range(10): # Split data into train and test sets X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=None) # Train Random Forest classifier clf = RandomForestClassifier(n_estimators=100) clf.fit(X_train, y_train) # Evaluate classifier accuracy = clf.score(X_test, y_test) accuracies.append(accuracy)print("Mean Accuracy:", sum(accuracies) / len(accuracies)) In this case, every time you run the code you get a slightly different value of the accuracy. This is due to variability in the algorithm’s process that, in this particular case, is due to randomness in bootstrapping that estimates the distribution of a statistic.

Note that flakiness due to the algorithms’ variability involves different ML models (not only the Random Forest).

Strategies to Avoid Flaky Tests in Machine LearningAs we’ve considered flakiness due to ML models’ management, in this paragraph we’ll consider some strategies to avoid it.

Strategy 1: Ensure ReproducibilityOne strategy to avoid flakiness in ML is to ensure reproducibility across ML experiments.

To do so, you can consider the following:

  • Set the random_state parameter. The random_state parameter in scikit-learn serves as a seed for the random number generator. It ensures that you get the same results each time you run a piece of code, which is crucial for reproducibility in machine learning experiments. Common choices are integers like 0, 42, or any other fixed integer. In particular, if you’re comparing different algorithms, preprocessing steps, or model parameters, use the same random_state across all the experiments to ensure that the differences in results come from the changes you’re investigating, not the randomness in data shuffling or initialization.
  • Version controlling data and code. Setting a fixed value of the random_state parameter allows you to study for flakiness due to other factors. Good tests can be made on the actual data at your disposal, as well as on the code you’ve written. To do so, a good practice is to create different experiments and freeze them, using version control for each different experiment: this way you can investigate different scenarios being sure to save everything. Finally, consider that there are also AI tools that can help you with version control like DVC.
  • Documenting the experiments. To ensure you remember your choices during experiments, a good practice is to document everything you did and why you did it. This way, the reproducibility is also granted on the side of the process.

Strategy 2: Stabilize The Training ProcessThe training process can introduce flakiness if you don’t follow standardized procedures. A strategy that eliminates flakiness can consider the following:

  • Hyperparameters tuning. Fixing and validating hyperparameters is a procedure that should always be followed. Leaving models with untuned hyperparameters leads to not reproducible tests and experiments.
  • Using consistent preprocessing techniques. To ensure reproducibility and avoid flakiness, data preprocessing is a must to ensure the data you have does not affect randomness. Feature Scaling, for example, ensures a consistent scaling of the features across different runs by using techniques like Min-Max scaling or Standardization (if you’re not familiar with the concept of scaling features, you can read this article).
  • Validate models across multiple runs. As flakiness is distributed across multiple processes and parameters in ML, validating models across multiple runs ensures reproducibility. To do so, a common choice is to use cross-validation for hyperparameter tuning. For example, k-fold cross-validation should be performed with the same number of folds and random_state across multiple runs. This helps in obtaining stable estimates of model performance by averaging results over different data splits.

Strategy 3: Implement Quality Assurance PracticesEnsuring the reliability and stability of ML workflows is fundamental to mitigating flakiness in ML. In this scenario, a good practice is to introduce quality assurance best practices to identify potential sources of variability and randomness in ML experiments.

For example, you can consider the following:

  • Unit testing for data preprocessing. Develop unit tests to validate data preprocessing steps, including feature scaling, encoding, and handling missing values. By testing these components individually, in fact, you can ensure consistent data transformations across different runs, reducing the risk of flakiness due to data preprocessing variability.
  • Integration testing for model training and evaluation. Conduct integration tests to validate the end-to-end ML pipeline, encompassing model training, validation, and evaluation stages. Integration tests, in fact, simulate real-world scenarios by validating the interaction between different components of the ML system, such as data loading, model fitting, and performance evaluation.
  • CI/CD pipelines for automated validation. Implement CI/CD pipelines to automate the validation process across multiple runs of ML experiments that integrate unit tests and integration tests to continuously monitor the stability and consistency of ML workflows. By automating the validation process, in fact, you can detect flakiness in the early stages of the development cycle and ensure the reproducibility of results across different environments. A good way to implement such a solution is to use the Semaphore flaky dashboard
  • Monitoring and logging for anomaly detection. Implement monitoring and logging mechanisms to track the behavior of ML models in production environments by continuously monitoring key performance metrics and logging anomalies or deviations from expected behavior. By proactively detecting anomalies, in fact, you can identify potential sources of flakiness and take corrective actions to maintain the stability of ML systems.

ConclusionsIn this article, we’ve shown how flaky tests can affect ML models.

Due to the stochastic nature of the ML models, there are a lot of ways to reduce and mitigate the effect of randomness. To do so, we’ve investigated how ML models can be affected by randomness and strategies to reduce it, by standardizing processes and procedures.

The post Flaky Tests in Machine Learning: Challenges and Countermeasures appeared first on Semaphore.

View Details

Ever spent hours writing a test, only to have it fail mysteriously the next day? If yes, you are not alone, It is sincerely a very common frustration for developers of all experience levels.

Let me share a little of my experience. Right before an update for our app, we ran our weekend tests to ensure everything worked fine. While earlier tests for purchasing items worked fine, on Sunday, they suddenly failed. After looking into it, we figured out that our tests were dependent on the response from a mock payment , programmed to mimic weekdays only, overlooking weekend transaction delays.

We never noticed this before because we never ran these tests on a weekend. It was a good lesson that our tests need to be more like real life, including what happens on weekends, to catch problems like this.

GoalsUnderstanding flaky tests is coming to terms that your application could be all perfect and still fail a test, it doesn’t matter if it has passed any previously, it could fail if it wants to, there are certain reasons for this, and there are certain ways we could enhance our test stability, one of which is by implementing test doubles and knowing when exactly to implement it, and thats the sole purpose of this article.

What is Flaky Test and Why Test Doubles?Similar to how stunt doubles do dangerous work in movies, we use test doubles to replace troublemakers and make tests easier to write.~Jani Hartikainen

A test that exhibits both passing and failing outcomes under the same configuration is known as a Flaky test. This tends to be very frustrating. Enhancing test stability and managing Flaky tests becomes secondary in a software development cycle.

Flaky test reduces confidence in the testing team and leads to wasted time and resources. A few of the most common causes of flakiness are Concurrency Issues, Dependency on External Services, leaked state, Platform or environmental differences, randomness, and fixed time dependency.

Employing the use of test doubles is a good choice to improve test stability, and make your tests more reliable.

They help by taking the place of unpredictable parts of your code, like external services, so your tests don’t fail unexpectedly. This approach saves time and makes sure your tests really check what they’re supposed to.

Test doubles are assumed objects that imitate the behavior of real components in your application. In other words, Test doubles are like fake parts in a system that act like the real ones, but in a way we can control.

They’re used in tests to take the place of real parts to make sure tests run the same way every time. “Test double” covers different kinds: stubs, mocks, fakes, spies, and dummies.

Types of Test DoublesTest doubles could be confusing at times and misplaced on when to be used and more especially what to be used.

Dummy ObjectsDummy Objects also known as Dummies, are usually put into parameters or arguments. These are passed around to satisfy the API but never actually used. For better understanding, they are like placeholders or fill-ins for parts of your code that need something to work, but you don’t use those parts for anything important.

Think of them as the extras in a movie scene they’re there to fill the space, so everything looks right, but they don’t have lines or impact the scene. They help your code run without errors when it’s expecting certain pieces to be there, even though those pieces aren’t doing anything.

Here is a basic implementation of a Dummy in Javascript :

const dummyObject = {};// Test using the dummytest('dummy test', () => { // Dummy is not used in this test}); In the code example above, we defined a dummy object named ‘dummyObject’, which could be usually empty. The purpose of this object is to fulfill the parameter requirement, without necessarily having any effect.

It is important to note that using a dummy object doesn’t directly address flakiness, but it helps ensure that the test remains consistent and predictable.

Spy ObjectsSpies are stubs that record some information based on how they were called. They can be used to verify that certain methods were called. In testing, you can liken spies to undercover agents, they watch and register what happens in the code without really getting involved. While they can mimic certain actions like stubs, their main job is to keep track of how and when they’re called.

Let’s look at a simple implementation of Spies:

// TEST WITH JESTconst inventoryService = { checkStock: () => Promise.resolve(true),};test("spy test", async () => { const spy = jest.spyOn(inventoryService, "checkStock"); const isInStock = await inventoryCheck("item123", inventoryService); expect(spy).toHaveBeenCalledWith("item123"); expect(isInStock).toBe(true); spy.mockRestore();}); Inside the test above, we use jest.spyOn() to create a spy on the checkStock() method of inventoryService. The spy records function calls and makes sure the behavior always resolves to true.

By ensuring expected behavior and function calls, spies contribute to test stability, and highly reduces the likelihood of flakiness in our test.

Fake ObjectsFakes do have a working implementation but usually take shortcuts which makes them unsuitable for production. They work and give results like they are the real thing, but take shortcuts to be easier to use in tests.

Let’s take for example, using an in-memory database instead of a real one speeds up tests but wouldn’t work for a live app because it’s too simple or not robust enough. Unlike spies, fakes are more about simplifying things by acting as a lightweight version of the real deal, making tests easier to manage.

Lets look at a simple implementation of Fakes:

// Fake Example with Jestclass InventoryServiceFake { checkStock(itemId) { return Promise.resolve(true); }}test("fake test", () => { const inventoryServiceFake = new InventoryServiceFake(); return inventoryCheck("item123", inventoryServiceFake).then((isInStock) => { expect(isInStock).toBe(true); });}); In the example above, we define a simple implementation of the InventoryService class called InventoryServiceFake. This fake implementation provides a simplified version of the checkStock method, which always resolves to true.

By using a fake object with a constant predictable behavior, we eliminate dependencies on external systems that might introduce flakiness into our test. The test then becomes more deterministic as it always expects true from the fake service. Is it a good practice? Of course it is.

Mock ObjectsMocks in tests act like parts of your code that expect certain actions to happen. They’re set up to watch for specific methods or functions to be called in a certain way. Instead of returning data like some other test doubles, mocks check if the right interactions occur. This means you’re testing how different parts of your code talk to each other, making sure everything is connected correctly.

Let’s look at a simple imlementaton of mocks in Jest. Imagine we have a class that retrieves user data from our API. This class utilizes Axios to make the API call and then returns the data attribute, which encompasses all the users. This could potentially be flaky because it depends on an external network request via Axios.

import axios from "axios";class Users { static async all() { try { const response = await axios.get("https://api.example.com/users"); return response.data.users; } catch (error) { console.error("Error fetching users:", error); return []; } }}export default Users; Now in other to test this method to completely isolate it’s dependence on external factors, We will use jest.mock(‘axios’) to mock the entire axios module.

We then use axios.get.mockResolvedValue(resp) to mock the get method of Axios to resolve with a mock response. The test asserts that the fetched users match the expected users. Thereby making it less prone to flakiness.

import Users from "./Users";import axios from "axios";jest.mock("axios");describe("Users", () => { test("fetching users should return expected data", async () => { const users = [{ name: "Alice" }]; const resp = { data: { users } }; axios.get.mockResolvedValue(resp); const fetchedUsers = await Users.all(); expect(fetchedUsers).toEqual(users); });}); Stub ObjectsStubs in tests can be likened to actors that follow a script. You tell them what to say (or how to respond) when they’re asked something specific during a test. They don’t do anything unexpected; they just give the responses you’ve set up prior to the response or in advance.

This helps make your tests much more predictable by ensuring that parts of your code that are not being tested don’t cause any unexpected results. Fakes are close to stubs but are often used instead of stubs because of their simplicity. The stub example will be seen as the article goes on.

Implementing Test DoublesIn my opinion, there are four ways we should implement test doubles to get the best results. We could start up by:

  1. Identifying tests that are flaky due to external dependencies or complex setups.
  2. Being able to choose the most appropriate test doubles for the job based on the nature of the test and the dependency.
  3. Replace the external dependency with the chosen test double.
  4. Refactor the test to use the test double, focusing on clarity and ensuring that the test accurately reflects the intended behavior, and after implementing the test doubles monitor the test for improvements and make adjustments if necessary.

Problems and Test Double SolutionsIn this section, we will be looking at four theoretical Test problems and Test double solutions.

  1. Isolation from External DependenciesProblem: Tests that interact with external systems that is databases, and APIs can fail. This is due to unpredictable issues that are outside the control of the test environment. It could be a network issue or an external service downtime.

Solution: Stubs and Fakes can simulate these external systems, ensuring that tests run consistently without being affected by external factors.

  1. Controlled BehaviorProblem: One of the causes of Flakiness in tests is dynamic data. Dynamic data could change between test runs.

Solution: Mocks and stubs can be programmed with a fixed response, ensuring that tests receive the same input and follow the same execution path every time, eliminating different results in tests.

  1. Time-based FlakinessProblem: Tests relying on real-time clocks or waiting for certain time-based events can fail if the timing isn’t precise on every run. Factors like system load, network delays, or scheduling differences can cause tests to fail even if the code itself is correct.

Solution: Using test doubles like stubs and spies to simulate time or events allows tests to run consistently regardless of real-world timing issues.

  1. Resource-intensive testsProblem: Some tests may be flaky because they exhaust system resources (memory, CPU) when running under certain conditions.

Solution: Fakes can be employed to provide lightweight implementations of heavy dependencies, reducing the resource load and avoiding conditions that lead to flakiness.

This is a good way to solve resource intensiveness but it is exposed to limitations, and here is why. If the flakiness shoots from internal resource consumption within your own code, unfortunately, fakes won’t directly solve the problem! You will then need to optimize your code’s resource usage.

Practical Application Of Test Doubles to Ehance Test StabilityIn this section, we will briefly look at a practical implementation of a Test double that could enhance Test stability thereby managing Flakiness in our test:

Problem: Network Dependency and External Service ReliabilityTo address the problem with network dependency and external service reliability, we’ll use a stub. Let’s take for example a function called inventoryCheck(). This function is designed to check if an item is in stock by using an external inventory service which could be flaky. This is what the example looks like:

function inventoryCheck(itemId, inventoryService) { return inventoryService .checkStock(itemId) .then((isInStock) => { return isInStock; }) .catch((error) => { console.error("Error checking inventory:", error); return false; });} Let’s go ahead and employ a stub:

class InventoryServiceStub { checkStock(itemId) { return new Promise((resolve) => { resolve(true); }); }}// Test using the stubtest("inventoryCheck with stub should return true for any item", () => { const inventoryServiceStub = new InventoryServiceStub(); return inventoryCheck("item123", inventoryServiceStub).then((isInStock) => { expect(isInStock).toBe(true); });}); In the code above our test is written with the Jest Framework. The class is a stub for the real inventory service. It simulates the behavior of an inventory service but in a simplified manner.

The checkStock() method in this stub doesn’t perform any real inventory check. Instead, it returns a promise that resolves with true, indicating that any item passed to it is considered to be in stock. This stub is used in testing to isolate the inventoryCheck function from external dependencies, allowing for a controlled environment to test the function’s logic and thereby manage Flakiness in our test.

Thoughts To NoteWill test doubles help solve flakiness in your test? The answer is yes, but while test doubles are a valuable tool for managing flaky tests;

  • They should be used simultaneously with techniques like Identifying the core root cause of flakiness (which could be network issues, external system bugs), and Refactoring code to be more deterministic and less dependent on external factors.
  • Misusing test doubles can potentially lead to cascading failures in a couple of ways. If the doubles don’t accurately mimic the behavior of the components they’re replacing, tests might pass erroneously, hiding real issues that could cause failures in production.
  • Over-reliance on test doubles is not advised. It’s crucial to balance the use of test doubles with integration and end-to-end tests to ease these risks.
  • Test doubles won’t detect API contract changes, so ensure you have separate integration tests that interact with the real API.

Closing ThoughtsTest doubles to a great extent helps, in fact if used properly would solve a mass majority of flakiness, but also understanding the simple cause of test fails could go a long way. Thank you for staying this long, I really appreciate your time. Let’s keep testing!

The post Enhancing Test Stability With Test Doubles for Flaky Test Management appeared first on Semaphore.

View Details

IntroductionThis tutorial guides you through various strategies to optimize CI/CD workflows in Semaphore, though many of these practices are also applicable to other CI/CD tools.

Our focus here is on techniques that minimize resource consumption and make your workflows run faster. By the end of this guide, you will understand how to improve feedback loops and project performance through dependency caching, fail-fast strategies, auto-cancel policies, and conditional job executions.

PrerequisitesBefore starting, you should have:

  • An active Semaphore account.
  • A foundational understanding of CI/CD principles.

Reducing Build Times with Dependency CachingChallenge: A considerable portion of build time in CI/CD pipelines is often consumed by reinstalling dependencies that rarely change, which delays feedback and uses resources unnecessarily.

Solution: Adding Dependency Caching

Two commands, cache store and cache restore can be used to manage dependency caching.

Example: Implementing Caching in a React Project

```

semaphore.yml snippet for caching npm dependenciesblocks: - name: "Install dependencies" task: prologue: commands: - checkout - cache restore $SEMAPHORE_GIT_BRANCH-$(checksum package.json), $SEMAPHORE_GIT_BRANCH, cache-master jobs: - name: npm install commands: - npm install - cache store $SEMAPHORE_GIT_BRANCH-$(checksum package.json) node_modules

`` This configuration checks for changes inpackage.json` and uses the cache when there are no updates, reducing unnecessary installations.

Learn more about caching and its strategies in the documentation.

Using Fail-Fast Strategies to Conserve ResourcesChallenge: Continued execution of jobs after a failure in one job can lead to wasted computational resources.

Solution: Implementing Fail-Fast

Semaphore includes options to immediately stop executing other jobs when a failure is detected, which helps to preserve resources.

Example: Implementing Fail-Fast in your Workflow

```

semaphore.yml snippet for fail-fast strategyversion: v1.0name: Example Semaphore CI pipelineagent: machine: type: e1-standard-2 os_image: ubuntu2204fail_fast: stop: when: "any"

``` This configuration cancels all pending jobs if any job fails.

We can also configure this by using the Workflow Builder:

Learn more about fail fast and its strategies in the documentation.

Canceling Redundant BuildsChallenge: Multiple pipelines running for several commits on the same branch can lead to redundant builds.

Solution: Setting Up Auto-Cancel

The auto-cancel feature helps manage resources by canceling redundant pipeline executions.

Example: Auto Cancel all new commits on existing branch

```

semaphore.yml snippet for auto-cancel policyauto_cancel: queued: when: "branch_has_new_commit"

``` This setting cancels all queued jobs on a branch when a new commit is pushed.

We can also configure this by clicking on the pipeline and going into the Auto-Cancel tab using the Workflow Builder:

Learn more about auto cancel and its strategies in the documentation.

Optimizing Execution with Conditional Job RunsChallenge: Executing jobs when no relevant code changes have occurred increases build times and resource use.

Solution: Using Conditional Job Execution

The change_in function allows jobs to run only when specific changes are detected in the codebase.

Example: Run Tests Block only if folder tests has changed

```

semaphore.yml snippet for conditional job executionblocks: - name: "Run Tests" run: when: "change_in('/tests/')"

`` This configuration runs the test block only if there are changes in thetests` directory.

We can also configure this using the Workflow Builder, by selecting a block and defining the Skip/Run conditions:

Learn more about the change_in command and its strategies in the documentation.

VideoCheck out the accompanying video to see these optimizations in action

ConclusionBy implementing the strategies detailed in this guide, you can decrease build times and operational costs in Semaphore. The techniques provided in this guide can be adapted to other CI/CD platforms as well.

The post A Guide to CI/CD Costs Optimization appeared first on Semaphore.

View Details

Today we are introducing new machine types, Apple Silicon M2 machines. These machines bring reduction of build and test times for developers working on macOS, iOS or any other platform type supported by Apple Hardware.

Who are these for?The new machines are suitable for all projects that are creating applications for Apple’s Ecosystem and use XCode 15 and above.

New Machines Performance BenchmarkWe’ve conducted benchmarks comparing the new M2 machine types against our existing AMD64 ARM machines using the XCode Benchmark repository.

M2 vs ARM64 build performance on Semaphore.Using the new a2-standard-4 machine type (Apple Silicon M2) can reduce your build times by up to three times.

Apple Silicon M2 machine build on Semaphore versus AMD64.Getting Started with Apple Silicon MachinesTo utilize the M2 Silicon machines in your Semaphore projects, specify the machine type in your agent configuration:

version: 1.0name: Apple Based Pipelineagent: machine: type: a2-standard-4 os_image: macos-xcode15 Or use the Workflow Builder to select the machine type:

Please note that the a2-standard-4 machine type only supports the macos-xcode15 image.

ConclusionThe new Apple Silicon machine types reduce build times for developers building applications for macOS, iOS, or any other platform supported by Apple’s hardware.

If your codebase supports XCode 15, you can switch to new a2-standard-4 machines starting today!

For more information check out the documentation.

Follow our YouTube channel for future videos.

Have a benchmark you want to share with us or just want to talk? Join our Discord !

The post Announcing Apple Silicon Machine Types appeared first on Semaphore.

View Details

Before now, the concept of containerization was left solely for the DevOps engineers. However today, the average developer is expected to have a basic understanding of how to containerize their applications. With the most popular containerization tool being Docker.

With this increasing demand for containers, there are some concepts in Docker that most engineers don’t pay atttention to. One of such concepts involves the management of sensitive data in containers. You can observe this in the way most developers hardcode sensitive data like API keys, database credentials, and other secrets directly into their Dockerfiles or environment variables.

This is bad practice and can lead to security vulnerabilities. This article will guide you through the best practices for managing sensitive data in Docker containers. It will teach you Docker’s built-in secrets management feature and how to use it to secure your applications. By the end of this article, you’ll know how to securely pass your sensitive data to your containers.

PrerequisitesThis article will contain a lot of hands-on examples and practical demonstrations. To follow along, you should have the following:

  • Basic understanding of containerization especially Docker.
  • A computer with Docker installed. Use the official Docker installation guide to install Docker on your machine.
  • Basic understanding of container orchestration concepts (optional, for the Docker Swarm section).
  • Docker Compose installed on your machine. You can use the official Docker compose installation guide to install Docker compose.

What are Docker Secrets?Docker secrets is a feature for Docker containers, that allows you to use and store sensitive data in a safe and secure way. These sensitive data could be SSH keys, API keys, database credentials, or any other piece of information that should not be shared with everyone who has access to the container.

By default, data transmitted between Docker containers is not encrypted or protected, so it can potentially be intercepted and read by someone with access to the network. This is why Docker secrets comes into the mix, to provide the security needed for your data.

Why Do you Need Docker Secrets?In a situation where you have one of your secrets, say your TLS certificates stored in plain text within your Dockerfile or in your Docker image. This means that anyone who has access to your Dockerfile or Docker image can easily access this secret. This is why you need Docker secrets.

Coupled with its main function of providing security, there are other reasons why you need Docker secrets. Some of these reasons include:

  • Superior Security Over Environment Variables: Docker secrets are generally considered more secure for storing sensitive data compared to environment variables. Docker secrets are stored in a virtual filesystem managed by Docker, and they are only accessible to the containers that have been granted access to them. Environment variables, on the other hand, are visible to anyone with access to the container, including processes running within the container.
  • Single Storage Location: Docker secrets are stored in a single location on the Docker host, and they are accessible to all containers running on the host. This makes it easy to manage secrets across multiple containers which is usually the case when using Docker Swarm or Docker Compose.
  • Ease Of Management: Although not utilized as often, using Docker secrets isn’t rocket science. It’s quite easy to manage and use Docker secret via the Docker CLI.
  • Abstraction Between Containers and Secrets: In a case where you have multiple environments, say, test, local and production. By using Docker secrets, your containers don’t need to be aware of the specific credentials used in each environment. Instead, they rely on a consistent secret name, abstracting away the underlying details. This flexibility allows you to deploy the same container image across multiple environments without modification.

How do Docker Secrets Work?You can only use Docker secrets when working with a collection of containers such as in Docker Swarm or Docker compose. When you add some secrets to a Docker Swarm, Docker will use a mutual TLS connection to securely pass these stored secrets to the Swarm manager. These secrets are stored in something called a Raft log, which is like an encrypted record-keeping system.

The Raft log along with the secrets are then replicated across all the managers in the Swarm. This ensures that even if one manager fails, the secret and other management data remain available.

Figure 1. Flow diagram for Docker secrets architectureWhen you want a service to use a secret, Docker takes the encrypted secret from the Raft log, decrypts it, and mounts it into the container’s memory as a special kind of file system. This file system is only accessible to the service’s containers, and it’s mounted as a read-only file system.

By default, in Linux containers, this mounted secret is placed in /run/secrets/<secret_name>, while in Windows containers, it’s put in C:\ProgramData\Docker\secrets. However, you can choose a different location if needed.

Nodes in the Swarm (the machines running Docker containers) only have access to encrypted secrets if they are Swarm managers or if they are running tasks for services that have been granted access to those secrets.

When a container stops running, any secrets it had access to are removed from the in-memory filesystem of that container and wiped from the node’s memory.

In the case where a node loses connection to the Swarm while running a task with access to a secret, the task can still access the secrets it had. However, it won’t receive updates until it reconnects to the Swarm.

How to Create a Docker SecretDocker provides a set of commands that make it easy to create and manage secrets. By running the docker secret createcommand, you can create a new secret and store it in the Docker host. However, there are two ways you can use this command.

  • Using standard input: This is the simplest way to create a secret. The syntax for this is:

echo "mysecretpassword" | docker secret create my_secret -kotjiwncv3kp8uyyj8d7kzesn * You’re creating a secret named my_secret and passing the secret data mysecretpassword through a pipe | from the echo command. Docker will then read the secret data from standard input. * Using a file: You can also create a secret from a file. The syntax for this is:

docker secret create my_secret_file /path/to/my_secret_filel95k6p8h7q6xgji9lxh164ibs In this case, you’re passing the my_secret_file to the docker secret create command. Docker will then read the secret data from the file and create a secret named my_secret_file.

The kotjiwncv3kp8uyyj8d7kzesn and l95k6p8h7q6xgji9lxh164ibs are the IDs of the secrets you just created. You can use these IDs to reference the secrets in other Docker commands.

To view your newly created secret, run the docker secret ls command. You should have a similar output:

ID NAME DRIVER CREATED UPDATEDkotjiwncv3kp8uyyj8d7kzesn my_secret 3 minutes ago 3 minutes agol95k6p8h7q6xgji9lxh164ibs my_secret_file About a minute ago About a minute ago How to Manage Docker SecretsThe docker secret command provides a set of subcommands that allow you to manage secrets. Below are some of the most commonly used subcommands:

  • docker secret ls: As seen above, this command lists all the secrets stored on the Docker host.
  • docker secret inspect: This command provides detailed information about a specific secret. You can use it to view the metadata of a secret, such as its ID, name, and creation date.

docker secret inspect my_secret [ { "ID": "kotjiwncv3kp8uyyj8d7kzesn", "Version": { "Index": 11 }, "CreatedAt": "2024-03-19T12:07:44.93980171Z", "UpdatedAt": "2024-03-19T12:07:44.93980171Z", "Spec": { "Name": "my_secret", "Labels": {} } } ] * docker secret rm: This command is used to remove a secret from the Docker host. You can use it to delete a secret by passing the secret ID or name as an argument.

docker secret rm my_secretmy_secret * After running this command, the secret with the name my_secret will be removed from the Docker host.

Integrating Docker Secrets into Docker SwarmDocker secrets are only available to services that are running on the same Docker Swarm. This means that you can only use Docker secrets with services that are part of a Docker Swarm.

If you’re not familiar with Docker Swarm, it is a container orchestration tool that allows you to manage a cluster of Docker hosts.

To use Docker secrets with Docker Swarm, you need to create a secret and then grant a service access to that secret. You can do this by adding the --secret flag to the docker service create command.

The following demo will create a redis service with a secret on Docker Swarm.

  • Initialize a Docker Swarm cluster. If you haven’t already done this, run the following command:

docker swarm init * Create a secret using the docker secret create command:

echo "mysecretpassword" | docker secret create redis_password -qg8cpym1q5v96jg67qkj4q5rc * Create the redis service and grant it access to the secret using the --secret flag:

docker service create --name redis --secret redis_password redis:alpinep3rmff7x5ta4gi4804cp9suj3overall progress: 1 out of 1 tasks1/1: running [==================================================>]verify: Service converged In this command, you’re creating a service named redis and granting it access to the redis_password secret. The redis:alpine image is used to create the service.After running this command, the redis service will be created and granted access to the redis_password secret. The secret will be mounted into the service’s containers and made available to the application.

  • Confirm that the redis service is running:

docker service ps redis * You can see if the secret is mounted on the redis container using the command below:

docker container exec $(docker ps --filter name=redis -q) ls -l /run/secretstotal 4-r--r--r-- 1 root root 16 Mar 19 12:51 redis_password * To stop the redis service from having access to the secret, use the following command:

docker service update --secret-rm redis_password redisredisoverall progress: 1 out of 1 tasks1/1: running [==================================================>]verify: Service converged After running this command, the redis service will no longer have access to the redis_password secret.

  • Confirm the service no longer has access to the secret by running the command in step 5.

docker container exec $(docker ps --filter name=redis -q) ls -l /run/secretsls: /run/secrets: No such file or directory * Once you’re done with the demo, you can remove the service using the following command:

docker service rm redisredis Integrating Docker Secrets into Docker ComposeAs mentioned earlier, Docker secrets can only work on multiple containers and Docker compose allows you to run multiple containers at once. This means that you can use Docker secrets with Docker Compose.

There are two syntaxes for using Docker secrets with Docker Compose. They are the long and short syntax. The long syntax gives you room to specify several attributes for the secret, while the short syntax allows you to specify just the name of the secret.

The following demo will use the short syntax and apply a secret to a redis service using Docker compose.

  • Create the file(s) to store your secret value. You can call this file my_secret.txt and store the secret value in it.

echo "mysecretpassword" > my_secret.txt * Create a docker-compose.yml file with the following content:

version: "3.9"services:redis: image: redis:alpine secrets: - my_secretsecrets:my_secret: file: ./my_secret.txt In this file, you’re creating a service named redis and granting it access to the my_secret secret. The redis:alpineimage is used to create the service. The my_secret secret is created using the file attribute and the value is read from the my_secret.txt file.

  • Run the following command to start the redis service:

docker-compose up -d After running this command, the redis service will be created and granted access to the my_secret secret. The secret will be mounted into the service’s containers and made available to the application.

  • Confirm that redis is running:

docker-compose psName Command State Ports---------------------------------------------------------------ubuntu_redis_1 docker-entrypoint.sh redis ... Up * You can see if the secret is mounted on the redis container using the same command from the Docker Swarm section.

docker container exec $(docker ps --filter name=redis -q) ls -l /run/secretstotal 4-rw-rw-r-- 1 1000 redis 17 Mar 19 18:12 my_secret The redis secret is now mounted into the redis container and made available to the application.

ConclusionThe main takeaway from this article is that secrets management is a critical aspect of container security. By using Docker secrets, you can securely store and manage sensitive data in your containers.

This article goes in-depth to explain the inner workings of Docker secrets, how to manage them, and how you can integrate secrets into Docker Swarm and Docker Compose. By following the examples provided, you’ll be able to store your secrets securely and use them in your applications.

The post A Complete Guide to Docker Secrets Management appeared first on Semaphore.

View Details

Imagine being back in high school, eagerly preparing for the big end-of-year project that will determine your final grades. You and your classmates are really working hard on various parts, each responsible for an important part of the project assigned to them. However, there’s a catch – some students are inconsistent in their contributions (We know them!). They promise to deliver their work on time, but when the deadline approaches, they falter. The project becomes a chaotic mess, with last-minute adjustments and patches to cover the gaps left by unreliable team members.

Now, let’s transpose this scenario into the world of software development, where flaky tests play the role of unreliable classmates. Developers invest time and effort into creating a robust testing suite, only to find that some tests behave inconsistently. They worksometimes and some other times, they don’t. These flaky tests can disrupt the smooth flow of development, destroy confidence in the testing process, and ultimately hinder the delivery of a stable program or application. In this article, we will walk through the common issue of flaky tests, their detrimental impact, and most importantly, proactive strategies to prevent them from even coming up in the first place.

Test InstabilityThe crazy thing about flaky tests is that without changes to the code, a test that passed previously may not pass again when being tested and may pass again later. Flaky tests, exactly like the inconsistent high school classmates, are a source of frustration for developers.

Flaky tests are like unpredictable troublemakers in the software world. They don’t consistently do what they’re supposed to, making developers scratch their heads when things go wrong. Picture this: a test fails randomly, and developers are left wondering, “Is there a real problem, or is it just playing tricks on us?” Dealing with these uncertain tests eats up a lot of time and energy, creating confusion and frustration.

In a field where dependability is important, flaky tests shake the foundation of developers’ trust in their testing tools. This lack ofconfidence creates a domino effect, causing delays in getting things done, reducing how much work can be accomplished, and, in the end, lowering the overall quality of the software being developed. It’s like having a mischievous teammate that throws a wrench into the gears when you least expect it.

Common Causes of FlakinessDependencies on External ServicesOne common culprit behind flaky tests is their reliance on external services. Consider a scenario where a test interacts with a remote API. Let’s say you connected the Stripe API to manage your payment. If the API experiences downtime or responds sluggishly, the test may fail intermittently. Let’s examine this with a code example using a Nest.js project:

import { Injectable } from '@nestjs/common';import axios from 'axios';@Injectable()export class FlakyService { async getDataFromExternalAPI(): Promise<string> { try { const response = await axios.get('https://stripeapi.example.com/data'); return response.data; } catch (error) { // Handle errors appropriately return 'error'; } }} In this example, the getDataFromExternalAPI method interacts with an external API. However, if the API is unreliable, the test could fail, highlighting the impact of external dependencies. Projects depend on external services a lot, however the behaviors of theseexternal services are inherently unpredictable. It might respond promptly and accurately one moment, only to falter with delays orerrors the next. This unpredictability becomes a breeding ground for flaky tests, especially when the test success depends on the external services responsiveness. In the example cited above, the inherent unpredictability of the external service is encapsulatedin the try…catch block, where we gracefully handle potential errors. However, such error-handling mechanisms, while necessary, do not eliminate the root cause of flakiness.

To address the unpredictability of external services, a good approach involves isolating tests from these dependencies. One effective strategy is to use mocking to create controlled, predictable responses during testing. Let us take a look at the modifiedversion of the example we cited above.

In this modified version, the FlakyService now uses the HttpService from Nest.js for making HTTP requests. This is possible thanks to dependency injection. In this test setup, the HttpService is replaced with a mock that returns predictable data. This method has effectively isolated the test from the the external service’s variability, creating a stable environment for our tests to thrive.

import { Test, TestingModule } from '@nestjs/testing';import { FlakyService } from './flaky.service';import { HttpService, HttpModule } from '@nestjs/common';import { of } from 'rxjs';describe('FlakyService', () => { let service: FlakyService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ imports: [HttpModule], providers: [ FlakyService, { provide: HttpService, useValue: { get: () => of({ data: 'mocked data' }), }, }, ], }).compile(); service = module.get<FlakyService>(FlakyService); }); it('should return mocked data', async () => { const result = await service.getExternalData().toPromise(); expect(result.data).toEqual('mocked data'); });}); Race Conditions in Parallelized TestsParallelizing tests for speed is a common practice, but it can introduce race conditions, leading to flaky outcomes. Imagine twotests manipulating shared resources concurrently. The order of execution can affect the results, causing intermittent failures. Mostdevelopers parallelize their tests to significantly reduce the time and effort in the testing process. it leverages automation testing toexecute the same tests in multiple environments, simultaneously. While this concurrency speeds up the testing process, it introduces the potential for race conditions — situations where the outcome of a test depends on the timing and order of execution.

Let’s take a look at an example:

let sharedValue = 0;test('User registration should create a new user', () => {});test('User login should succeed with valid credentials', () => {}); Now taking a look at this, in a parallelized environment, if the login test is executed before the registration, the login might fail, dueto the absence of a registered user. And if it does succeed before the registration test, you actually have hidden a bug with a falsepositive result.

To solve the challenges of race conditions, a key strategy is test isolation. Each test should run in isolation, independent of other tests, making sure that the outcome is solely determined by the test itself and not influenced by the order of execution. This wayyou can avoid total misleading failures and false positives.

Let’s look at a possible solution for our implementation above. In the modified version below, the resetDatabaseToOneUserfunction ensures a clean state before each test, mitigating the impact of race conditions. By isolating the test, you have a better assurance that about your tests.

beforeEach(async () => { // Include a function that ensures a clean state for each test await resetDatabaseToOneUser();});test('User login should succeed with valid credentials', () => {}); Another way is to make use of asynchronous patterns. When used within your tests, you can control the flow and timing ofoperations, which in turn minimizes the chances of unexpected interleaving. See the example below:

test('User login should succeed with valid credentials', async () => { // Simulate user login process using asynchronous operations await performAsyncLogin(); // ...}) Inconsistent Test EnvironmentsInconsistencies between development and testing environments can also breed flaky tests. This is because there are differences in data, configurations, or dependencies. While having different environments is good software practice, this can also cause test issues.

Inconsistent test environments arise when there are disparities between the conditions under which tests are written and those inwhich they are executed. These disparities can manifest in various forms, including differences in data, configurations, or dependencies. Let’s take a look at an example below:

import { Injectable } from '@nestjs/common';@Injectable()export class FlakyService { getData(): string { return process.env.NODE_ENV === 'production' ? 'production-data' : 'development-data'; }} In this example, the getData method returns different values based on the environment. In a production environment, it mightfetch data from a live database, while in a development environment, it could use mocked or sample data.

Some efficient ways to manage environment consistency includes, implementing a robust configuration management to ensureuniformity across environments. Use configuration files or environment variables to control settings like database connections, API endpoints, and feature flags. Another way is to using containerization tools like Docker, which provide a consistent environment, reducing the likelihood of discrepancies between development and production setups. Another way as well, is to integrate continuous integration into your workflow. CI tools, such as Semaphore, ensure that tests run in an environment mirroring production conditions. This helps catch inconsistencies early in the development process.

Imagine a scenario where a team member develops a feature on their local machine, running tests against a local database. The tests pass, and the feature is marked as ready for deployment. However, in the staging environment, which mirrors production closely, the absence of a specific database record causes a test to fail. This discrepancy, arising from inconsistent testenvironments, could have been mitigated by adopting some efficient measures. Let’s take a look at a code example below, wherewe can modify the test environment to mimic production conditions, ensuring consistency in data and configurations:

import { Test, TestingModule } from '@nestjs/testing';import { FlakyService } from './flaky.service';describe('FlakyService', () => { let service: FlakyService; beforeEach(async () => { process.env.NODE_ENV = 'production'; // Simulating production environment const module: TestingModule = await Test.createTestingModule({ providers: [FlakyService], }).compile(); service = module.get<FlakyService>(FlakyService); }); it('should return production data', () => { const result = service.getData(); expect(result).toEqual('production-data'); });}); This way, tests run in a production-like environment during the testing phase, mitigating the risk of flaky tests caused by environmental inconsistencies.

Proactive Strategies for Robust TestsNow that we have seen some common causes and how to handle them. Let’s take a look at preventive measures, strategies that ensure that flaky tests, do not get the opportunity to fly. Let’s take a look at a few!

Test IsolationTest isolation involves the separation of tests to ensure that their execution is independent of each other. Each test should operate in isolation, with no reliance on the outcome or state of other tests. This not only makes cleaner test code but also protects against the propagation of failures, minimizing the chances of flaky tests.

import { Test, TestingModule } from '@nestjs/testing';import { FlakyService } from './flaky.service';describe('FlakyService', () => { let service: FlakyService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [FlakyService], }).compile(); service = module.get<FlakyService>(FlakyService); }); it('should increment the counter', () => { service.incrementCounter(); expect(service.getCounter()).toBe(1); }); it('should reset the counter', () => { service.resetCounter(); expect(service.getCounter()).toBe(0); });}); Here it is evident that the test 2 is not reliant on test 1 and vice versa. This way, we ensure that the success or failure of one test doesn’t impact the execution of others, promoting a stable testing environment.

Data ManagementData management is equally important. The probability of having flaky test, could be as a result of uncontrolled or inconsistent data, which may also introduce variability. In the example below, the second test doesn’t rely on the first test to create a user, ensuring both test isolation and proper data management. Managing data properly within each test, we can ensure that each test is self-contained, reducing dependencies and minimizing the chances of flaky outcomes.

import { Test, TestingModule } from '@nestjs/testing';import { FlakyService } from './flaky.service';describe('FlakyService', () => { let service: FlakyService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [FlakyService], }).compile(); service = module.get<FlakyService>(FlakyService); }); it('should create a user and return the user object', async () => { const user = await service.createUser('testuser'); expect(user).toBeDefined(); }); it('should delete a user by user object', async () => { const user = await service.createUser('testuser'); await service.deleteUser(user); const userCount = await service.getUserCount(); expect(userCount).toBe(0); });}); Test-Driven PrinciplesTest-Driven Development (TDD) is more than just a methodology; it’s a philosophy that places testing at the forefront of the development process. TDD principles can help developers proactively shape the design and functionality of their code while simultaneously ensuring the reliability of their tests. Let’s look at a practical application. Imagine we want to build a basic calculator service with addition and subtraction functionalities:

import { Injectable } from '@nestjs/common';@Injectable()export class CalculatorService { add(a: number, b: number): number { return a + b; } subtract(a: number, b: number): number { return a - b; }} Now, let’s implement tests for these functionalities using TDD:

import { Test, TestingModule } from '@nestjs/testing';import { CalculatorService } from './calculator.service';describe('CalculatorService', () => { let service: CalculatorService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [CalculatorService], }).compile(); service = module.get<CalculatorService>(CalculatorService); }); it('should add two numbers', () => { const result = service.add(2, 3); expect(result).toBe(5); }); it('should subtract two numbers', () => { const result = service.subtract(5, 2); expect(result).toBe(3); });}); In this example, we write tests before implementing the actual functionalities. TDD encourages a cycle of “Red-Green-Refactor,” where we start with a failing (Red) test, implement the minimum code to make the test pass (Green), and then refactor the code while keeping the tests passing. This approach minimizes the chances of test flakiness, as tests are designed to reflect the expected behavior of the code.

Test Environment Consistency – Creating a Stable FoundationTest environment consistency is a cornerstone of reliable testing. Inconsistent environments between development, testing, and production can introduce variables that lead to flaky tests. Creating a consistent test environment ensures that tests produce reliable results, irrespective of the context in which they are executed. Let’s take a look at an example:

In this example, the ExternalApiService dynamically sets the API endpoint based on the environment. Let’s write tests for this service, ensuring that the environment consistency is maintained:

import { Test, TestingModule } from '@nestjs/testing';import { ExternalApiService } from './external-api.service';describe('ExternalApiService', () => { let service: ExternalApiService; beforeEach(async () => { process.env.NODE_ENV = 'test'; // Set the environment to 'test' const module: TestingModule = await Test.createTestingModule({ providers: [ExternalApiService], }).compile(); service = module.get<ExternalApiService>(ExternalApiService); }); it('should fetch data from the test API', () => { const result = service.fetchData(); expect(result).toBe('data from external API'); }); afterEach(() => { process.env.NODE_ENV = 'development'; // Reset the environment to 'development' after each test });}); In the example above, we set the environment to ‘test’ before running the tests and reset it afterward. This ensures that the testsconsistently use the same environment settings, preventing variability that could lead to flaky outcomes.

Advanced Techniques for Maximum StabilityThese techniques, include Property-Based Testing and Chaos Testing, which elevate our testing practices, ensuring that our tests remain steadfast even when there are complexities and uncertainties.

Property-Based Testing Property-Based Testing (PBT) takes a really different approach than traditional unit testing. Instead of specifying expected outcomes for specific inputs, PBT explores the properties or invariants that should hold true across a range of inputs. PBT uncovers edge cases and potential issues that might go unnoticed in example-based testing. It does this , by generating a diverse set of inputs. It checks that a function, program or whatever system under test abides by a property. Let’s take a look at an example:

Assume we have a simple function to reverse arrays:

import { Injectable } from '@nestjs/common';@Injectable()export class ArrayUtilsService { reverseArray<T>(arr: T[]): T[] { return [...arr].reverse(); }} Now, let’s use PBT to ensure the reversal function maintains a crucial property — reversing an array twice should yield the originalarray:

import fc from 'fast-check';import { Test, TestingModule } from '@nestjs/testing';import { ArrayUtilsService } from './array-utils.service';describe('ArrayUtilsService', () => { let service: ArrayUtilsService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ArrayUtilsService], }).compile(); service = module.get<ArrayUtilsService>(ArrayUtilsService); }); it('should satisfy the reversal property', () => { fc.assert( fc.property(fc.array(fc.integer()), (arr) => { // Reversing twice const reversedTwice = service.reverseArray(service.reverseArray(arr)); // Verify that the reversed array is equal to the original array const areEqual = JSON.stringify(arr) === JSON.stringify(reversedTwice); // Verify that the reverseArray method really reverses the array const isReversed = JSON.stringify(arr) === JSON.stringify(service.reverseArray(arr)); // Check if both conditions are true return areEqual && isReversed; }) ); });}); Here, the fc.property function generates random arrays and verifies that the reversal property holds true. PBT allows us to test a broad range of scenarios, uncovering potential issues that might go unnoticed in example-based testing.

Chaos TestingChaos Testing is a practice where deliberate, controlled chaos is injected into a system to assess its resilience. By introducing faults and disruptions, developers gain valuable insights into how well their systems can recover from adverse conditions.

Let’s simulate chaos in an HTTP service using the chaos-monkey-engine library:

import { Injectable, HttpService } from '@nestjs/common';import { Observable } from 'rxjs';@Injectable()export class HttpServiceService { constructor(private readonly httpService: HttpService) {} fetchData(): Observable<any> { return this.httpService.get('https://api.example.com/data'); }} Now, let’s introduce chaos testing by randomly delaying HTTP requests:

import { Test, TestingModule } from '@nestjs/testing';import { HttpServiceService } from './http-service.service';import ChaosMonkey from 'chaos-monkey-engine';describe('HttpServiceService', () => { let service: HttpServiceService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [HttpServiceService], }).compile(); service = module.get<HttpServiceService>(HttpServiceService); }); it('should handle delayed HTTP requests under chaos', async () => { const chaosMonkey = new ChaosMonkey(); chaosMonkey.enable(); const response = await service.fetchData().toPromise(); chaosMonkey.disable(); expect(response).toBeDefined(); });}); In this example, Chaos Testing is simulated by enabling the ChaosMonkey to randomly delay HTTP requests during the test. Thischaos injection allows us to observe how well our service responds under adverse conditions.

Tools and Frameworks That Support Property-Based Testing and Chaos TestingTo facilitate Property-Based Testing, tools like;

  • Fast-check for JavaScript – https://www.npmjs.com/package/fast-check
  • Hypothesis for Python – https://hypothesis.readthedocs.io/

offer robust support.

For Chaos Testing, frameworks such as;

  • Chaos-monkey-engine for JavaScript – https://www.npmjs.com/package/chaos-engine
  • Chaos Toolkit for Python – https://chaostoolkit.org/

provide controlled chaos injection capabilities.

Why continuous integration and continuous delivery is importantContinuous Integration (CI) and Continuous Delivery (CD), automate the testing and deployment processes, ensuring that changes are continuously validated and seamlessly delivered to production.

  1. Early Detection of Flakiness: Continuous Integration (CI) ensures that tests are run automatically with each code commit. This early and frequent testing helps detect flaky tests immediately, allowing developers to address issues promptly. This makes it easier to address issues before they escalate and that any deviations from the expected behaviour is identified as early as possible and not piled up.
  2. Consistent Testing Environments: CI promotes consistent testing environments by automating the setup and configuration of test environments. This minimizes discrepancies between testing environments, which ensures dependable and reproducible test results.
  3. Automated Deployment to Staging: Continuous Delivery (CD) extends the benefits of CI by automating the deployment & testing of code changes to & on staging environments. This automation ensures that flaky tests are validated in environments closely mirroring production, reducing the chances of environment-related flakiness.
  4. Feedback Loop for Developers: CI/CD provides a rapid feedback loop for developers. Quick turnaround times in the testing and deployment processes enable developers to iterate on their code, fix flaky tests efficiently, and maintain the overall stability of the testing pipeline.
  5. Scheduled Daily/Weekly Runs: This gives extra validation of the system regularly. This way with frequent tests, developers can identify and address flaky tests that may not be caught during regular CI/CD processes. Adding this in the CI/CD process adds an extra layer of assurance, allowing developers to catch and address flakiness that may occur over time or under specific conditions.

Building a Testing Culture Within Your TeamThe foundation of test stability is laid not just in tools and techniques but in the culture of your development team. Pushig for a testing culture involves promoting collaboration, knowledge sharing, and a shared responsibility for test quality. Here are a few tips, for cultivating a healthy testing culture in your team:

  1. Knowledge Sharing: Encourage team members to share their testing knowledge through regular meetings, workshops, or documentation. Cross-functional expertise strengthens the team’s overall testing capabilities.
  2. Collaborative Code Reviews: Incorporate testing considerations into code reviews. Reviewing tests alongside the code helps identify potential issues early in the development process.
  3. Pair Programming: Foster collaboration through pair programming sessions, where team members work together on writing tests and implementing features. This collaborative approach enhances testing skills across the team.
  4. Learning Opportunities: Provide learning opportunities for team members to explore advanced testing techniques, tools, and methodologies. This can include workshops, training sessions, or participation in relevant conferences.
  5. Celebrate Testing Achievements: Acknowledge and celebrate achievements related to test stability. Recognizing the efforts of team members in maintaining a reliable test suite reinforces the importance of testing within the team.

ConclusionTackling flaky tests before they fly is extremely important. In this article, we went over the problems they pose, from uncertainty to eroded confidence. Proactive strategies, like isolating tests and managing data, act as shields against flakiness. Advanced techniques, such as property-based and chaos testing, push testing boundaries. Integrating continuous testing into development workflows not only catches flaky tests early but also cultivates a culture of reliability.

Having reliable automated tests is like having a trusty guard for our software. It ensures our applications work well and are less likely to run into problems. These tests act as a safety net, giving us the confidence to make updates and improvements without worrying about breaking things. So by minimizing flaky tests, we boost trust in our testing process, saving us valuable time and effort.

The post How to Stop the Flakes Before They Fly appeared first on Semaphore.

View Details

We’re rolling out updates to Semaphore’s Role-Based Access Control (RBAC) that add project-specific roles, user groups, and the option to include external collaborators who aren’t on GitHub or Bitbucket.

Previous RBAC SetupPreviously, Semaphore synced directly with GitHub and Bitbucket. If you were part of an organization or any projects on these platforms, you automatically had access to all its projects on Semaphore with the same level of permissions. This setup didn’t allow for different access levels; if you had access, you had full control.

New RBAC Setup Project Roles and User GroupsNow, you can assign specific roles at the project level, such as Reader, Contributor and Admin:

  • Reader: Readers can access the project page, view workflows, their results, and job logs. However, they cannot make any modifications within the project.
  • Contributor: Can view, rerun, change workflows and ssh into jobs. Can promote and view insights, schedulers, etc.
  • Admin: Admins have the authority to modify any setting within the projects, including the ability to add new individuals, remove them, or even delete the entire project.

This means you can set up access based on what team members need to do, like giving deployment permissions to team leads or read-only access to CFOs for billing info.

User groups let you organize team members into categories and manage their permissions collectively at the project level. This setup allows you to apply access controls to groups instead of individual users. It’s practical for larger teams or projects where similar roles need consistent access, helping you maintain a clean permission structure throughout your project.

Adding External CollaboratorsA key update is the ability to add users to projects even if they aren’t part of your GitHub or Bitbucket organizations. This is great if you want to give your CFO read only access to billing or assign engineering managers the ability to oversee deployments.

Continued Integration with GitHubWe’re still syncing permissions with GitHub and Bitbucket. You can keep using Semaphore as you always have while taking advantage of these new features.

Why This MattersWith these updates, you can better control who accesses your projects and what they can do and you can also organize them in groups.

This is particularly useful for larger teams or when roles vary widely across different projects.

Conclusion These updates to Semaphore’s RBAC make it easier to manage who can access and control your projects. They help you improve security by allowing more precise access settings.

Check out these new features by signing up or upgrading your Semaphore plan and check our documentation.

Follow our YouTube channel for future videos and join our Discord to give us feedback and suggestions!

The post Announcing RBAC Enhancements – Project Roles and Groups appeared first on Semaphore.

View Details

Technical writing is the art of communicating complex ideas in simple terms. Boiling it down to the basics and then building it back up, little by little, until you said all that had to be said.

In these days, where Generative AI is everywhere, I firmly believe technical writing skills are needed more than ever before. We need people with the discipline to check every sentence for accuracy. We need people who take the cluttered, fancy language produced by ChatGPT and strip it down to plain English.

Over the last few years, we gave talks to aspiring technical writers. In this post, we’re releasing the remastered talks. We hope you enjoy them as much as we did making them.

Technical Writing for Humans (P.S. I Love You)Dan Ackerson, CTO of TeleClinic and most excellent technical reviewer shares his unique and elegant method for approaching technical writing: Technical Writing for Humans

Technical Writing for FreelancersWe have to talks here. The first one, is a good starting point for freelancing technical writers. It covers the basics of the trade and finishes with tips for making a living as a writer.

The second talk is all about writing engaging articles. Technical writing doesn’t have to be boring! In fact, I believe a good writer can make interesting any topic. In this talk, I cover techniques to grab the reader’s attention and keep momentum until the end.

Want more?If all that wasn’t enough for you, there’s more:

  • I gave this talk in Conf42’s DevSecOps 2023: Technical writing vs AI. Is it still a worthy career path?
  • My first ever talk (in Spanish) for Nerdear.la 2021: Escritura tecnica en ingles

The post Tips for Becoming a Good Technical Writer appeared first on Semaphore.

View Details

In React development, Axios and Fetch streamline API communication but maintaining stable API tests can be tricky. Flaky tests, prone to occasional failures, pose a common challenge. This article explores strategies to combat flakiness, ensuring reliable testing in React projects.

Importance of API Testing in React ProjectsReact’s ability to connect with external APIs makes it ideal for dynamic apps. Reliable API communication is essential for a smooth user experience, and that’s where API testing ensures flawless data interactions in React projects.

API testing is crucial in React projects for several reasons:

  • Integration Verification: API testing checks if the frontend and backend of a website or app work together properly. It makes sure that the parts of the app that users see (like buttons and text) talk correctly to the parts users don’t see (like databases or other services). For example, it makes sure that when you click a button in a React app, it talks to the right place and gets the right information.
  • Functionality Testing: It validates whether API endpoints return correct data formats, status codes, and responses, ensuring that React components consume API responses correctly.
  • Security Testing: API testing includes security testing to identify vulnerabilities, protecting both backend systems and React frontends from potential security threats.
  • Error Handling: API testing allows you to test error scenarios, such as when the API returns error responses or encounters validation issues. By simulating such scenarios, you can ensure that the React application handles errors gracefully and provides appropriate feedback to users.

Highlighting the Issue of Flaky Tests and Their Negative ImpactFlaky tests are a common problem in software development, often overlooked but with significant consequences. Below are some of the negative impacts of flaky tests:

  • Reduced Confidence: When tests are flaky, they act unpredictably; sometimes passing, sometimes failing without any changes to the code. This inconsistency makes developers doubt whether the tests accurately reflect the quality of their work.
  • Decreased Productivity: Dealing with flaky tests consumes valuable time and resources, diverting developers’ focus from productive tasks to troubleshooting and debugging.
  • Delayed Bug Detection: Flaky tests can make it hard to spot real problems because they randomly pass or fail. This confusion delays fixing actual bugs, slowing down the process of improving the software.
  • False Sense of Security: Flaky tests sometimes pass unexpectedly, making developers think everything’s fine with their code. It’s like getting lucky on a test you didn’t study for—it might give you a false sense of confidence. This can lead developers to overlook real issues in the code.

Popular HTTP Client Libraries in React: Axios and FetchFor making HTTP requests in React applications, developers have two popular choices: Axios and Fetch. These libraries streamline communication with APIs and backend services, enabling developers to prioritize building exceptional user interfaces.

Axios is a popular library in React known for its concise and user-friendly approach to making HTTP requests. It allows developers to perform various actions like fetching data, sending updates, and more, using clear methods like GET, POST, and DELETE. Axios simplifies asynchronous operations through its promise-based API, making it ideal for interacting with RESTful APIs. It provides features such as interceptors for request and response handling, making it highly customizable and versatile.

On the other hand, Fetch is an inbuilt browser API for making HTTP requests, widely supported across modern browsers. It offers a straightforward interface for fetching resources and supports promises, making it compatible with modern JavaScript patterns. Fetch is lightweight and built into the browser, eliminating the need for additional dependencies in React projects. However, Fetch’s simplicity also means it lacks some advanced features found in Axios, such as request cancellation and global error handling.

Both Axios and Fetch have their strengths and weaknesses, and the choice between them often depends on the specific requirements of the project. While Axios provides a more feature-rich and user-friendly experience, Fetch offers a lightweight and native solution for basic HTTP requests. Ultimately, developers should evaluate their project’s needs and preferences when selecting the most suitable HTTP client library for their React applications.

Understanding Flaky TestsIn this section, we’re going to take a closer look at what flaky tests are all about. Whether you’re new to software testing or an experienced developer, understanding flaky tests is crucial. So, let’s dive in.

Definition of Flaky TestsFlaky tests are those unstable things in the world of software testing that occasionally pass or fail without any changes to the codebase. They are like the chameleons of the testing world, changing their results seemingly at random. Flakiness slows down the development process and decreases trust in test outcomes. Below are some characteristics of flaky tests:

  1. Inconsistency: Flaky tests exhibit inconsistent behavior, passing or failing under different conditions or environments.
  2. Unpredictable: Unlike reliable tests that produce consistent outcomes for the same input, flaky tests behave unpredictably, making it challenging to pinpoint the root cause of failures.
  3. External Dependencies: Tests relying on external resources like databases, APIs, or third-party services are prone to flakiness due to factors beyond the developer’s control, such as network latency or service availability.

Common causes of flakiness in API tests (e.g., network issues, race conditions, external dependencies)Flakiness in API tests can be a source of frustration for developers, resulting in unreliable test outcomes and wasted time spent diagnosing false positives or negatives. Below, are some of the factors contributing to API flakiness:

Network Issues: Network instability can have a major impact on the reliability of API tests. Frequent test failures can be caused by fluctuations in data loss, delays in the network, or server failures, which might cause unexpected responses or timeouts. Testing environments with different network conditions can produce varying results, making it difficult to pinpoint the reasons for failures. To tackle network issues, testers can use techniques such as simulating API responses, using placeholders or virtual services, or conducting tests in controlled network setups. By separating tests from external factors, teams can achieve more consistent and trustworthy results. Here’s an example code demonstrating how to simulate API responses using Axios:

// Simulating API responses (using Axios)axios .get("/api/data") .then((response) => { // Simulate a network delay return new Promise((resolve) => setTimeout(() => resolve(response.data), 2000), ); }) .then((data) => { // Your test assertions here }) .catch((error) => { // Handle errors appropriately });// Using a mocking library (e.g., nock) with Fetchnock("https://api.example.com") .get("/data") .reply(200, { message: "Mocked response" });fetch("https://api.example.com/data") .then((response) => response.json()) .then((data) => { // Your test assertions here }) .catch((error) => { // Handle errors appropriately }); The first code snippet simulates an API response with a 2-second network delay using Axios, while the second snippet uses a mocking library (like Nock) to intercept actual API calls and provide a predefined response, ensuring tests remain consistent and independent of external APIs.

Race Conditions: In API testing, race conditions occur when test results depend on the timing or order of events. This happens when several requests occur simultaneously or during asynchronous operations. Such situations can lead to unpredictable behavior because the test outcome may vary based on which request completes first, particularly if both are trying to modify the same resource at once. Testers can address race conditions by scheduling operations, employing wait mechanisms, or inserting delays between requests. Additionally, designing tests with independent operations and clear boundaries can minimize the impact of race conditions on test results. Here’s an example code showcasing how to schedule operations with Axios:

// Scheduling operations with Axiosasync function fetchData() { const response1 = await axios.get("/api/data1"); const response2 = await axios.get("/api/data2"); // Use the data from both responses // Alternatively, use Promise.all for parallel execution with control const [response1, response2] = await Promise.all([ axios.get("/api/data1"), axios.get("/api/data2"), ]);}// Using async/await with Fetchasync function fetchData() { const response1 = await fetch("/api/data1"); const response2 = await fetch("/api/data2"); const data1 = await response1.json(); const data2 = await response2.json(); // Use the data from both responses} These code examples show fetching data from two APIs sequentially (one after another) using either Axios or Fetch with async/await. They don’t involve timed scheduling, but rather managing asynchronous calls effectively.

External Dependencies: API tests often require external services, databases, or third-party APIs to imitate real-world interactions. Changes or disruptions in these dependencies can disrupt tests. For example, updates to API endpoints or service outages can lead to unexpected failures. Testers should use less of these external factors by imitating or replacing them whenever possible. This prevents test failures caused by changes or disruptions in external dependencies. Here’s an example code demonstrating how to mock external dependencies with Axios:

// Mocking external dependencies with Axiosconst mockData = { message: "Mocked response" };axios .get("/api/data") .then(() => Promise.resolve(mockData)) // Mock the response .then((data) => { // Your test assertions here }) .catch((error) => { // Handle errors appropriately });// Mocking external dependencies with Fetch (using nock)nock("https://api.example.com").get("/data").reply(200, mockData);fetch("https://api.example.com/data") .then((response) => response.json()) .then((data) => { // Your test assertions here }) .catch((error) => { // Handle errors appropriately }); These code snippets showcase mocking external dependencies (like APIs) during tests. The first example uses Axios, intercepting the request and resolving with predefined mockData. The second example uses Nock with Fetch, mimicking the API response at a specific URL (https://api.example.com/data) with the provided mockData. This allows testing to proceed without relying on actual external services.

By incorporating these code examples and techniques, you can significantly enhance the reliability and maintainability of your API tests in React projects.

Strategies for Mitigating Flaky TestsBelow are some of the strategies used in reducing flaky tests:

Network ResilienceNetwork resilience refers to the ability of a system to maintain functionality and performance in the face of network disruptions or failures. It involves implementing strategies to mitigate the impact of network-related issues on the system’s operation and reliability.

Implementing Retry Mechanisms with Exponential Backoff: Retry mechanisms with exponential backoff are a strategy used to handle transient failures in network communication. When a request fails due to network issues, the system automatically retries the request after a brief delay, with each subsequent retry increasing the delay exponentially. This approach allows the system to wait for temporary issues to be resolved before retrying, reducing the likelihood of repeated failures and improving overall reliability. Here’s an example of implementing retry logic with exponential backoff using async/await:

async function fetchDataWithRetry(url) { let retries = 0; const wait = (delay) => new Promise((resolve) => setTimeout(resolve, delay)); while (retries < 3) { // Adjust retry limit as needed try { const response = await axios.get(url); return response.data; } catch (error) { console.warn(`Error fetching data, retrying (${retries + 1}/${3})...`); retries++; await wait(Math.pow(2, retries) * 1000); // Exponential backoff } } throw new Error("Failed to fetch data after retries");} The code above defines a function fetchDataWithRetry that attempts to fetch data from a URL. If the request fails due to network issues, it retries the request with an increasing delay between attempts (exponential backoff) to avoid overwhelming the server with repeated requests.

Stabilizing Tests through Mocking External API Responses: Mocking external API responses involves simulating the behavior of external dependencies, such as APIs, during testing. Instead of making real requests to external services, the tests use mocked responses that are predefined by the developer. This ensures that tests remain stable and consistent, even if the external APIs are unavailable or unreliable. By decoupling tests from external dependencies, developers can create a controlled testing environment that is immune to fluctuations in external services, reducing the occurrence of flaky tests. Here’s an example using Jest for mocking external API responses:

// Assuming a library like Jest for mockingjest.mock("your-api-module");test("MyComponent fetches data successfully", async () => { const mockedData = { message: "Success!" }; yourApiModule.fetchData.mockResolvedValue(mockedData); const component = render(<MyComponent />); await waitFor(() => expect(component.text()).toContain(mockedData.message));}); The code above demonstrates how to mock the behavior of an external API module (your-api-module) using Jest’s mocking functionalities. The test defines mocked data and configures the fetchData function of the mocked module to return this data. This allows the test to run in isolation from the actual API, ensuring stability even if the real API is unavailable or unreliable.

Code ImprovementsThis section focuses on enhancing the quality and reliability of tests by making improvements directly to the codebase.

Refactoring Tests to Isolate Dependencies: This means restructuring tests to separate dependencies like external services or databases from the main logic being tested. By relying less on external factors, tests become more stable and less likely to have problems because of changes in external systems. Here’s an example of isolating external service interaction in a separate function:

// Extract logic interacting with external service to a separate functionfunction getUserData(userId) { return fetch(`https://api.example.com/users/${userId}`);}test("MyComponent displays user data", async () => { const mockUserData = { name: "John Doe" }; jest .spyOn(window, "fetch") .mockResolvedValueOnce(new Response(JSON.stringify(mockUserData))); const component = render(<MyComponent userId={1} />); await waitFor(() => expect(component.text()).toContain(mockUserData.name));}); This code refactors the test logic to separate the functionality of fetching user data from the core component logic. The getUserData function encapsulates the interaction with the external service, making the test more focused on the component’s behavior and less reliant on external factors.

Harnessing the Power of Test Doubles (Stubs and Spies) for Enhanced Control: Test doubles, such as stubs and spies, give developers strong tools to manage dependency behavior during testing. Stubs give pre-decided responses to method calls, while spies help monitor and check interactions with dependencies. Using test doubles lets developers control test scenarios better, leading to more reliable and predictable test results. Here’s an example using Sinon for stubs/spies:

// Using Sinon for stubs/spies (replace with your chosen library)const sinon = require("sinon");test("MyComponent calls external service with correct parameters", () => { const spy = sinon.spy(window, "fetch"); render(<MyComponent />); expect(spy.calledOnce).toBe(true); expect(spy.firstCall.args[0]).toEqual("https://api.example.com/data");}); This code demonstrates how to use Sinon’s spy function to monitor interactions with the fetch function during the test. The test verifies that the component calls the fetch function with the correct URL, ensuring the component interacts with the external service as expected.

Test DesignIn test design, the focus is on creating effective and efficient test cases that accurately assess the behavior of the software under test. This involves two key sub-strategies:

Crafting Clear and Concise Test Cases Focused on Specific Behaviors: This approach highlights the need for clear and focused test cases. Testers should ensure that each case has a clear purpose, with defined expected outcomes and input conditions. This clarity helps accurately reflect the intended behavior of the software. Here’s an example of a test focused on error handling:

test("MyComponent renders an error message when data fetching fails", async () => { jest.spyOn(window, "fetch").mockRejectedValueOnce(new Error("Network Error")); const component = render(<MyComponent />); await waitFor(() => expect(component.text()).toContain("Error fetching data"), );}); This code defines a test that simulates a network error during data fetching and verifies that the component displays an appropriate error message to the user.

Organizing Tests Logically and Grouping Them by Functionality: This approach focuses on organizing test suites logically by grouping related tests based on the functionality they cover. It simplifies test management and execution, improving readability and maintainability. Testers can easily find relevant tests and understand dependencies between different software components. This approach is typically implemented through a test suite file structure. For instance:

// user.test.jstest('MyComponent displays user name', async () => { ... });test('MyComponent renders error for invalid user ID', async () => { ... });// data.test.jstest('MyComponent fetches data successfully', async () => { ... });test('MyComponent handles data fetching errors', async () => { ... }); These test files (.js) organize tests for a component (MyComponent) by functionality. user.test.js likely holds tests for user interactions (name display, invalid ID handling), while data.test.js focuses on data fetching scenarios (success, error handling). This separation improves code clarity.

Axios vs. Fetch for Flaky Test MitigationIn web development, minimizing flaky tests is crucial for maintaining code reliability. Choosing the right HTTP client, such as Axios or Fetch, can greatly impact this. Let’s explore how each helps mitigate flakiness in tests.

Comparing Axios and Fetch: Error Handling, Cancellation, and Request/Response ManipulationError Handling: Axios and Fetch handle errors differently. Axios provides built-in error-handling capabilities, allowing developers to easily catch and handle errors using interceptors. This simplifies error management and enhances the overall reliability of the application. Here’s an example of error handling with Axios:

import axios from "axios";axios.interceptors.response.use( (response) => response, (error) => { // Handle error here if (error.response) { // The request was made and the server responded with a status code // that falls out of the range of 2xx console.error(error.response.data); console.error(error.response.status); console.error(error.response.headers); } else if (error.request) { // The request was made but no response was received // `error.request` is an instance of XMLHttpRequest in browser environments console.error(error.request); } else { // Something happened in setting up the request that triggered an Error console.error("Error", error.message); } return Promise.reject(error); },);// Example usageaxios .get("https://api.example.com/data") .then((response) => { // Handle successful response }) .catch((error) => { // Handle error based on error object properties }); On the other hand, Fetch requires developers to manually check the response status and throw errors accordingly, making handling errors more difficult to understand and possibly more vulnerable to mistakes. Here’s an example of error handling with Fetch:

fetch("https://api.example.com/data") .then((response) => { if (!response.ok) { // Handle error using response.text() or response.json() response.text().then((text) => { console.error(text); }); throw new Error(`Request failed with status ${response.status}`); } return response.json(); }) .then((data) => { // Handle successful response }) .catch((error) => { // Handle error console.error(error); }); Cancellation: Canceling requests is important in web development, particularly when users move away from a page or do something that makes ongoing requests unnecessary. Axios makes canceling requests easy with a built-in cancellation token feature. This allows developers to cancel requests without hassle.

import axios from "axios";const source = axios.CancelToken.source();axios .get("https://api.example.com/data", { cancelToken: source.token }) .then((response) => { // Handle successful response }) .catch((error) => { if (axios.isCancel(error)) { console.log("Request canceled"); } else { // Handle other errors } });// Cancel the requestsource.cancel("Request is no longer needed"); On the other hand, Fetch doesn’t have built-in support for cancellation. Developers have to create their own solutions using the AbortController interface, which can be more complicated and prone to errors. Here’s an example of cancellation with Fetch (using AbortController):

const controller = new AbortController();const signal = controller.signal;fetch("https://api.example.com/data", { signal }) .then((response) => { if (!response.ok) { throw new Error(`Request failed with status ${response.status}`); } return response.json(); }) .then((data) => { // Handle successful response }) .catch((error) => { if (error.name === "AbortError") { console.log("Request canceled"); } else { // Handle other errors } });// Cancel the requestcontroller.abort(); Request/Response Manipulation: it’s common to need to change how requests and responses work. Axios makes this easy with its simple way of intercepting and adjusting them using interceptors. This allows for customizing things like how requests are sent or how responses are handled. Here’s an example of Request/Response Manipulation with Axios:

import axios from "axios";axios.interceptors.request.use((config) => { // Modify request config (e.g., add headers, authorization) config.headers.Authorization = `Bearer ${accessToken}`; return config;});axios.interceptors.response.use((response) => { // Modify response data (e.g., transform or filter data) return response.data.results; // Assuming response has a `results` property});// Example usageaxios .get("https://api.example.com/data") .then((data) => { // Data will be the transformed results }) .catch((error) => { // Handle error }); Fetch can also achieve similar functionality, but it usually involves writing more code using middleware functions. Here’s an example of Request/Response Manipulation with Fetch (using middleware functions):

async function fetchData(url) { const response = await fetch(url); if (!response.ok) { throw new Error(`Request failed with status ${response.status}`); } const data = await response.json(); // Modify response data before returning return processData(data);}async function processData(data) { // Transform or filter data return data.map((item) => ({ id: item.id, name: item.name.toUpperCase() })); // Example transformation}// Example usagefetchData("https://api.example.com/data") .then((data) => { // Data will be the transformed results (uppercase names) }) .catch((error) => { // Handle error }); As you can see, Axios provides a more concise and convenient way to manipulate requests and responses using interceptors. Fetch requires creating separate functions for manipulation, making the code potentially more verbose.

Exploring Flakiness Mitigation: Features and Approaches in Axios and FetchDeciding between Axios and Fetch can have a big impact on the success of the search to reduce flakiness in online apps. Let’s examine the features and methods used by each of these HTTP clients to mitigate flakiness using React samples.

Retries and Exponential Backoff: Retries are when a system tries again after an error, and exponential backoff is a strategy where the time between retries is increased to help ease congestion and improve success rates.Below we discuss how Axios and Fetch handles Retries and Exponential Backoff.

  • Axios: Axios, a tool for making HTTP requests, offers an easy way to handle retries using a technique called exponential backoff. With Axios’s interceptors feature, developers can set up automatic retries for failed requests. These retries happen at increasing intervals, which helps applications recover from temporary network problems more effectively, making them more resilient.
  • Fetch: With Fetch, developers can manually implement retry logic with exponential backoff using custom functions. While not as streamlined as Axios, Fetch provides flexibility in handling retries according to specific project requirements.

Error Handling: Error handling is how programs manage unexpected problems. It’s about detecting errors, deciding what to do, and finding ways to fix or work around them. This might involve using special tools like exceptions or codes to deal with errors when they happen. Below we discuss on how Axios and Fetch handles errors.

  • Axios: Axios simplifies error handling with its built-in interceptor for handling response errors. Developers can easily catch and handle errors in a centralized manner, improving code readability and maintainability.
  • Fetch: Error handling with Fetch requires developers to manually check the response status and throw errors accordingly. While this provides flexibility, it can lead to boilerplate code and potential inconsistencies in error handling.

Axios and Fetch both address flakiness in web apps. Axios offers built-in solutions like retries and error handling, while Fetch allows for customization. Developers should weigh their project needs and preferences when deciding between these libraries for flakiness mitigation in React apps.

Leveraging Axios and Fetch for Stable Requests: Strategies to Reduce Flaky TestsAxios and Fetch stand out as widely-used JavaScript libraries designed for managing HTTP requests, known for their effectiveness in reducing test flakiness. When it comes to stabilizing requests, developers often rely on retry mechanisms employing exponential backoff. Axios streamlines this process by offering built-in support for interceptors, facilitating the implementation of custom retry strategies such as exponential backoff. Similarly, Fetch provides the flexibility to incorporate custom retry logic through JavaScript’s Promise chaining mechanism.

Another strategy involves mimicking responses from external API. Both Axios and Fetch support this feature in tests. By doing so, developers can simulate the behavior of external services without actually relying on them. This decoupling of tests from external dependencies creates a controlled testing environment, it helps maintain consistent and predictable results, unaffected by external factors.

Arranging test groups in a clear way and putting similar tests together makes testing easier and more reliable. It helps keep things organized, makes tests easier to read, and shows how different parts of the program rely on each other. To make sure tests run smoothly and accurately, using tools like Axios and Fetch, along with techniques like retrying failed tests and creating fake versions of parts of the program, can be really helpful. This means that the software runs smoothly and reliably. By carefully planning and putting these methods into action, developers can be sure they’re making top-notch software they can trust.

ConclusionIt’s really important to handle unreliable API tests properly in React projects so that the app stays stable and dependable. Ways to do this include retrying tests when they fail, pretending the API responses during testing, and using helpful tools like Axios or Fetch. By doing these things, developers can reduce the chance of tests giving inconsistent results and make sure the software they build is strong and reliable. By being proactive and thinking carefully about how they test, React projects can keep working smoothly and give users a great experience.

The post Tackling Flaky API Tests in React Projects Using Axios or Fetch appeared first on Semaphore.

View Details

By challenging assumptions and embracing experimentation, individuals and teams can unlock fresh ideas. To this end, collaboration fueled by diverse perspectives further strengthens this innovation cycle. In this episode, Kelsey Hightower shares his experiences, from challenging the status quo in large organizations to embracing the collaborative spirit of open-source communities. Discover how Kelsey’s contributions to Puppet and his role in the development of Kubernetes shaped the landscape of modern infrastructure.

Edited transcriptionEschewing a traditional computer science degree heavy on theory, Kelsey prioritized practical skills. Certifications, contracting work, and even running his own computer store became his training ground. By the time he landed his first full-time job, Kelsey possessed a strong foundation and a desire to make a real impact.

Large organizations, however, proved a challenge. Accustomed to questioning processes and constant learning, Kelsey encountered a different reality. He found that “there’re so many people that walk around those kinds of environments that have 20 years of one year’s experience; They only know how to do the process, not change the process.”

But it was also around that time that Kelsey found the open-source community, which consolidated as the pivot of his tech career. Unlike his experience in his full-time job, the community provided him opportunities to interact with industry leaders, contribute to projects, and embrace the concept of “learning in public”: “It felt like a lot of the people who were designing these technologies, you could just jump online, IRC, mailing list, ask them questions, and they will take the time to respond,” Kelsey recalls.

As Kelsey identified areas in his job where automation could be applied, he actively sought and implemented open-source tools like Puppet. However, as he found, large enterprises are often cautious about adopting new technologies, especially ones that haven’t yet reached a certain level of maturity.

Undeterred, Kelsey turned to open-source contribution as a learning and experience engine. Nights and weekends were dedicated to honing his Python skills and actively participating in the community. “[I was] learning how to join, contribute at home,” he explains. “That’s when I opened the door to the community.” His expertise and contributions didn’t go unnoticed. Puppet Labs, impressed by his work, offered him a position, moving from a practitioner using open-source tools to a leader within the open-source space, working directly for an open-source company.

Moving to an open-source development companyWhile working at a large enterprise, Kelsey focused on maintaining existing systems. In turn, joining Puppet Labs meant “moving into the people who create things” and innovating. Despite the change, the transition felt natural. “I already knew most of the engineers,” Kelsey explains, “because I’d been contributing to the same codebase.” The same was true about decision-making in what was now a community-driven environment in which consensus was reached through open communication. “That’s when I opened my first Twitter account just to say, ‘Hey guys, we’re working on this. Please upvote the issue,’” Kelsey recalls.

Learning to collaborate and being open to acquiring new skills will get you further than labels, Kelsey affirms. “The industry gets hung up on labels,” he observes, and points out that “you can collaborate effectively regardless of your team’s name; Just calling yourselves ‘DevOps’ doesn’t guarantee results.”

Even the most brilliant idea needs buy-in. “You can’t just be right; You really have to do way more work to get everybody else on board to be successful,” says Kelsey, who learned that strong communication and alignment within the organization are what drive change.

On the other hand, working with a highly skilled team meant encountering plenty of good ideas. In this scenario, selecting the best solution inevitably led to situations where some good ideas wouldn’t be implemented, which could be demotivating for some team members: “That is a really hard challenge because smart people get told ‘no.’

Public speaking and social mediaIn open-source projects, influencing change requires effective communication to build consensus across a global community. “You have no choice but to be a communicator,” says Kelsey, highlighting the stark contrast to limited communication in enterprises. Building alone isn’t enough. Open-source engineers need to be storytellers, explaining their work through documentation, presentations, and more. “Any engineer that’s good, when you build something, your next step is to tell the world about it… There is no world where you build it and they come and figure out what it is,” Kelsey says.

Given the tight relationship between being a communicator and developer in the open-source sphere, public speaking at meetups and conferences felt like a natural extension for Kelsey. Although initially hesitant, he embraced social media like Twitter to share his work, answer questions, and connect with a global audience. Soon, his online presence blossomed into a respected reputation within the tech community.

From Puppet to containersKelsey’s friction with Puppet began with his push to migrate their codebase from Ruby (limited in scalability) to Go. Internal resistance arose due to concerns about enterprise compatibility with legacy systems. “So we’re that company now… trapped now in our own success…?” he thought, frustrated.

The final straw came with Docker’s emergence. This containerization paradigm offered a fresh approach to system administration, but Puppet hesitated to adopt it. Kelsey, eager to explore this new world, transitioned to a smaller company.

While Docker boasted a user-friendly experience, complexities arose in large-scale deployments. As such, CoreOS, Kelsey’s new company, developed Fleet, their own orchestration tool built on etcd and systemd. However, it still faced limitations imposed by Docker’s control. To solve this issue, CoreOS developed Rocket as its own container runtime, offering greater freedom.

However, around that time, Kubernetes came to light. A container orchestration tool built to address the shortcomings of existing solutions, Kubernetes introduced the concept of pods, groups of tightly coupled containers that could be managed as a single unit. Pod’s flexibility in deployments was paired with Kubernetes’ superior networking capabilities.

Consequently, despite its early dominance, Docker couldn’t hold onto the orchestration crown. Docker prioritized the developer experience and building a strong image ecosystem, a strategy that, while successful, wasn’t as fit for complex deployments. As Kelsey puts it, “You haven’t mastered a tool until you understand when it should not be used.” Docker eventually introduced orchestration features like Docker Swarm, but it couldn’t match the momentum Kubernetes had already built.

Several factors fueled Kubernetes’ success. Among them, its well-defined API stands out as what allowed the community to expand Kubernetes’ functionality and integrate it with diverse tools and platforms. Just like Linux distributions like Red Hat and Ubuntu empower adoption, Kubernetes benefited from open-source distribution ecosystems and the collective knowledge of a vast developer community.

Building an Open-Source Colossus: The Village that Raised KubernetesKubernetes, the ubiquitous container orchestration platform, isn’t the brainchild of a single genius. Instead, its success story is a testament to the power of collaboration in open-source communities. “I don’t know if anyone had the solo understanding,” Kelsey says, pointing out that a collaborative spirit is a cornerstone of successful open-source projects.

Interestingly, Kelsey sheds light on an unexpected way in which collaboration manifests: through competition. Companies like VMware, Heptio, and Microsoft Azure, despite being competitors, ultimately contributed to Kubernetes’ success. “Everyone was spreading out and saying, this is the way, this is the way,” says Kelsey. This lack of “arrogance to ignore”—even from competitors—embodies the open-source spirit. Diverse ideas, embraced from all corners, ultimately improve the project as a whole.

Drawing a parallel with Linux, another open-source giant project, Kelsey finds Kubernetes to be of a similar modular nature. Just as companies like Samsung can contribute specific modules to the Linux kernel, “CoreOS or Kubernetes gets the same treatment,” says Kelsey, highlighting contributions from various sources.

The bottom lineWhile Kelsey has recently retired from his full-time job, he remains actively involved in the tech community. Want to delve deeper into Kelsey Hightower’s work? Here are some starting points:

  • You can find Kelsey’s talks on Youtube.
  • Follow him on X at @kelseyhightower

The post Kelsey Hightower on Embracing Open Source appeared first on Semaphore.

View Details

If you find yourself juggling with your testing framework, making your development process more cumbersome than it should be, maybe it’s time to try out Vitest. The native test runner for Vite.

Why Vitest?When Vite came out, it stood out from the rest by offering an easy way to handle ESM imports (ECMAScript Modules). Vite provided a modern alternative to the older CommonJS, making your frontend code simpler and faster to render. However, Vite lacked a native test runner — until Vitest. Vitest is designed to be an ESM-native alternative to Jest.

Using both Vite and Jest means maintaining two separate pipelines: one for building your app and another for testing. Since Jest doesn’t natively support ESM imports, developers had to convert their code to CommonJS for testing, adding layers of complexity and configuration.

![[jest-pipeline.png]]

Vitest, on the other hand, uses the same pipeline, plugins, and configurations as Vite, drastically simplifying and speeding up the testing process.

Vitest featuresVitest comes packed with features, including:

  • ESM Imports and top-level awaits
  • Workspaces for monorepos
  • Plugins
  • TypeScript/JSX Support
  • A GUI Dashboard
  • In-Source testing
  • Coverage reports
  • Jest-compatible snapshots
  • Experimental Browser mode

These features make Vite Test a comprehensive solution for modern web development testing needs.

Getting Started with VitestLet’s start from a brand-new Vite project. I’ll start with a vanilla TypeScript demo:

$ npm create vite . Select a framework: › Vanilla Select a variant: › TypeScriptScaffolding project in /private/tmp/a...Done. Now run: npm install npm run dev To kick things off with Vitest, start by creating a new Vite project (e.g., using npm create vite).

Once your project is set up, we can install Vitest as a dev dependency:

$ npm install --save-dev vitest Creating some testsCreating your test files is straightforward. Vitest looks for test code in files named with .test or .spec. From there, you can import your modules and use familiar test Jest keywords, making the transition easy for many developers.

For example, our demo code has a counter component that increments each time the button is pushed:

// src/counter.tsexport function setupCounter(element: HTMLButtonElement) { let counter = 0 const setCounter = (count: number) => { counter = count element.innerHTML = `count is ${counter}` } element.addEventListener('click', () => setCounter(counter + 1)) setCounter(0)} To test this component we can create src/counter.test.ts with the following contents:

import { setupCounter } from './counter';import { describe, it, expect, beforeEach } from 'vitest';describe('setupCounter', () => { let buttonElement: HTMLButtonElement;}); Here we are importing the TDD keywords from “vitest” and creating a test block. Let’s add a beforeEach block to initialize the component before each test runs:

import { setupCounter } from './counter';import { describe, it, expect, beforeEach } from 'vitest';describe('setupCounter', () => { let buttonElement: HTMLButtonElement; beforeEach(() => { document.body.innerHTML = '<button id="test-button"></button>'; buttonElement = document.getElementById('test-button') as HTMLButtonElement; setupCounter(buttonElement); });}); Now we can add a test to check if the counter is initially zero:

import { setupCounter } from './counter';import { describe, it, expect, beforeEach } from 'vitest';describe('setupCounter', () => { let buttonElement: HTMLButtonElement; beforeEach(() => { document.body.innerHTML = '<button id="test-button"></button>'; buttonElement = document.getElementById('test-button') as HTMLButtonElement; setupCounter(buttonElement); }); it('initializes with count is 0', () => { expect(buttonElement.innerHTML).toBe('count is 0'); });}); Let’s add two more tests that click on the button:

import { setupCounter } from './counter';import { describe, it, expect, beforeEach } from 'vitest';describe('setupCounter', () => { let buttonElement: HTMLButtonElement; beforeEach(() => { document.body.innerHTML = '<button id="test-button"></button>'; buttonElement = document.getElementById('test-button') as HTMLButtonElement; setupCounter(buttonElement); }); it('initializes with count is 0', () => { expect(buttonElement.innerHTML).toBe('count is 0'); }); it('increments counter on click', () => { buttonElement.click(); expect(buttonElement.innerHTML).toBe('count is 1'); }); it('increments counter on multiple clicks', () => { buttonElement.click(); buttonElement.click(); buttonElement.click(); expect(buttonElement.innerHTML).toBe('count is 3'); });}); Before we can run this test, however, we need to import the document into the test environment. We can do that by installing jsdom:

$ npm install --save-dev jsdom And creating vite.config.js in the root of the project:

// vite.config.jsimport { defineConfig } from 'vite';export default defineConfig({ test: { environment: 'jsdom' }}); Note that we’re using the Vite config file here. Vitest supports using the same pipeline as Vite (with plugins and all!), so you only have to configure things in one place.

To finish setup, let’s add a "test" script in package.json:

{ ... "scripts": { "dev": "vite", "build": "tsc && vite build", "preview": "vite preview", "test": "vitest" } ...} We can start Vitest in watch mode with:

$ npm test ✓ src/counter.test.ts (3) Test Files 1 passed (1) Tests 3 passed (3) Start at 14:24:23 Duration 413ms (transform 28ms, setup 0ms, collect 26ms, tests 18ms, environment 548ms, prepare 78ms) PASS Waiting for file changes... press h to show help, press q to quit In-Source Testing: A Unique FeatureOne of Vitest’s unique features is in-source testing, inspired by Rust. This allows you to place your test code in the same file as your actual code, simplifying management and making tests more accessible.

Configuring Vite to support in-source testing involves a few steps. First, we need to configure Vitest to look for tests inside source files:

// vite.config.jsimport { defineConfig } from 'vite';export default defineConfig({ test: { environment: 'jsdom', includeSource: ["src/**/*.{ts,js}"] }}); Next, we need to add the importMeta line to tsconfig.json. This imports the Vitest metadata into the environment:

{ "compilerOptions": { ... "types": ["importMeta"], ... }} Finally, we can add our tests into the same file as the code using the following pattern:

// source code goes here ...import { describe, it, expect, beforeEach } from 'vitest';if(import.meta.vitest) { // your tests go here} This will ensure the tests only run when Vitest is invoked.

The final touch is to configure Vite to ignore the test code when creating a production bundle. To configure that, we need to add a define key in vite.config.js:

import { defineConfig } from 'vite';export default defineConfig({ test: { environment: 'jsdom', includeSource: ["src/**/*.{ts,js}"] }, define: { "import.meta.vitest": "undefined" }}); Now running npm run build will not include your tests in the bundle.

GUI SupportWe can view the test results in the browser by installing @vitest/ui:

$ npm install --save-dev @vitest/ui And adding --ui to the Vitest invocation in package.json

{ ... "scripts": { "test": "vitest --ui" } ...} Now running npm test will open a browser where we can see the results, filter tests and even re-run tests from the page.

Retry failed testsRetrying is a great way to detect flaky tests. To make Vitest retry failed test we can use the CLI option:

vitest --retry=20 Or via the Vite config:

export default defineConfig({ test: { retry: 20 },}); However, one problem with this approach is that Vitest does not log the failures, so we cannot identify Flaky tests with this method.

I’ve tried several options, including creating a custom reporter to force Vitest to show something when it retries but I’ve not found any solutions. If you have one, please let me know.

ConclusionEven if you are not using Vite, Vitest is still a great test runner and a decent alternative to Jest. Vitest, however, shines when coupled with Vite, since it greatly simplifies the project setup and makes things run much smoother.

Learn more about testing in JavaScript:

  • 4 Reasons Why You Should Prefer Vite Over Create-React-App (CRA)
  • Writing Unit Tests in Node.js Using Jest
  • How To Organize Constants in a Dedicated Layer in JavaScript

The post Vitest: Replacing Jest on Vite Projects appeared first on Semaphore.

View Details

Since the launch of the flaky tests in open beta, we’ve been working hard on polishing the interface and improving the detection heuristics. Today, we’re excited to announce that the flaky test dashboard is available to all Semaphore users.

So, What are Flaky Tests?Flaky tests are tests that oscillate between failing and passing without any clear reason. They tend to sneak up on you at the worst possible time, introducing unforeseen delays and making life harder for everyone.

The best place to spot flaky tests is in your CI/CD pipelines. But identifying them used to require a lot of manual work. Now, you can sit down and let Semaphore do that for you.

How Does It Work?Once this feature is enabled you will find the new dashboard in your project. Here, you can see the flaky tests in your codebase, apply filters to the data, and take action on your failed tests.

You can see the flaky test UI in action below:

How Do I Set It Up?To enable this feature, you will need to configure test reports in your project. This allows Semaphore to collect and aggregate information on your tests. Once this is done, the flaky test dashboard will be automatically populated as test data is processed.

For more details, check out the flaky test’s docs.

What Changed Since the Beta?If you’ve been trying out this feature in beta, thank you! Your feedback is pure gold to us. Since the beta, the following things have changed:

  1. The dashboard filters are populated with sane defaults when flaky tests are first set up.
  2. Notifications are now available. You can now get notified when new flaky tests appear in your suite.

Try It Out NowSign in to your Semaphore account and check out this new feature. We hope it helps you to combat the unpredictability of flaky tests in your projects.

Read more:

  • What is a Flaky Test? How to Fix Flaky Tests?
  • Best Practices for Identifying and Mitigating Flaky Tests
  • End-to-end Flaky test management on Cypress, Playwright and Selenium

The post Flaky Test Dashboard is Now Generally Available appeared first on Semaphore.

View Details

Software testing is vital in software development and maintenance. With a proper testing strategy, you can ensure the quality and reliability of your product. It’s not just the QA team that has to ensure the quality of the product; developers also need to write unit tests to ensure that the code they are shipping is working as per the requirement. Everything seems fine until your tests start failing, and you start debugging those tests to identify the root cause. Unfortunately, not all tests give consistent results and may fail without code change, also known as flaky tests. Flaky tests can occur in codebases of any size, and many tech giants, such as Facebook and Google have reportedly been plagued by it. Flaky tests can cause issues for software development teams, affecting the reliability of the test suite. This article explores practical strategies for identifying and reducing flaky tests to enhance testing processes.

What are flaky tests?Tests are typically deterministic, meaning they will always pass or fail for the same code. However, some tests are non-deterministic, also known as flaky tests. A test is flaky if it shows different behavior with the same code under the same environment. Flaky tests are like hidden landmines that can detonate unexpectedly and disrupt your testing process. It makes them problematic because they can show inconsistent results and undermine the reliability of the testing process.

The following image represents a typical CI/CD flow when a flaky test is detected in the nightly build pipeline:

  • The CI pipeline is triggered at a specific time.
  • Code is compiled and built.
  • CI runs the tests, and developers are notified if there is a failure.
  • If there are no failures, the code is deployed to the target environment.
  • If a test fails, it is rerun to verify the result.
  • If the rerun test still fails, it suggests that it is flaky, and then developers investigate the underlying cause and refactor the code to fix the test flaky test.
  • Rerun the modified test to verify that flakiness is resolved.
  • The code is deployed to the target environment if the test passes.

Best Practices to Fix Flaky TestsWhile no silver bullets exist, you can use the following best practices to reduce the number of flaky tests in your development process.

Regular Test MaintenanceIt is a good practice to schedule routine reviews of the test suite. This can eliminate redundant or unnecessary tests. It may happen that some of them were already flaky, so getting rid of those tests is a good thing. Regular test maintenance proactively addresses flaky tests and ensures the test suite remains healthy and reliable. By periodically examining test results, recognizing issues, and handling them promptly, we ensure that tests are up to date and that we don’t compromise on their quality.

Ignore itIgnoring or disabling a flaky test is tempting, but you should think logically before using this approach. Ideally, you should not ignore or delete them because it’s not like they are constantly turning your CI pipeline red. These decisions are often made when a flaky test in CI is blocking your release pipeline. You also have to understand that there is some functionality that a flaky test is testing, and that functionality works in cases when the CI pipeline is green. In Java, if you use the JUnit testing framework, you can use the @Disabled annotation to avoid running a particular test if you think it’s flaky. Similar functionality is available with most language test frameworks. However, you need to realize that this is just a short-term measure, and eventually, we must deal with that flaky test.

Isolation and IndependenceTo ensure accuracy, it is recommended that your tests be independent and isolated from external dependencies. If you do not do this, even though tests are functionally working correctly, they may fail just because something is wrong with that dependency. One popular test pattern for achieving isolation and independence is the Hermetic test pattern.

According to this pattern, each test should be self-sufficient, and any dependency on any test or third-party API that is not in your control should be avoided.

⁤In traditional shared testing environments, fixing flakiness in your tests is challenging due to the unavailability or issues with external dependencies(e.g., network issues). However, a hermetic testing environment is consistent, isolated, and deterministic, generating predictable test results. Think of it like a container running everything required for your tests.

Timeout StrategiesMicroservices have made end-to-end testing complex because your service now has numerous dependencies. To test such a complex setup, you need to rely on timeouts because your tests may hang indefinitely if you don’t get a response in time. You must configure a timeout strategy in your test to save resources, time, and effort.

This approach also has issues. For example, there is a long delay in a call to an external system, and since you have configured a timeout, the test will fail after the wait time exceeds the timeout. The test will be flagged as a flaky test. To fix this, you can rely on creating mocks for such external API calls where you have observed a delay in response, or the external system itself is frequently unavailable.

Randomized Test OrderRandomized test order is another approach to uncover dependencies between tests and flakiness issues related to test execution order. Let us understand this with an example.

In the following code, the output of testC depends upon the execution order of testB. If testC is executed sequentially after testB, then testC will pass. Otherwise, if the test execution order is randomized and testB executes after testC, then testC will fail.

import org.junit.Test;import static org.junit.Assert.assertEquals;public class RandomOrderTest { private static int num = 0; @Test public void testA() { assertEquals(1, num + 1); } @Test public void testB() { num = 10; assertEquals(12, num + 2); } @Test public void testC() { assertEquals(13, num + 3); // Passes if testB runs before testC, otherwise fails }} To fix these flaky tests, you should stop sharing their state. If you have a complex test setup and have to share state between tests, at least reset them after each run. Ideally, each test should tear up and tear down its environment independently. In Junit, you can use the @BeforeEach annotation to set the initial state before running each test method.

For example, you can fix the flakiness in the previous code sample by resetting the shared state before each test method is executed. Since we are resetting the state of the shared variable before each test, we need to slightly adjust testC to ensure it is independent of the test execution order of other tests.

@BeforeEach public void setUp() { num = 0; // Reset shared state before each test } @Test public void testC() { int actualResult = num + 13; assertEquals(13, actualResult); } Continuous MonitoringMost modern CI tools provide enough metrics to discover patterns in test failures. Some CI tools also allow you to rerun your test automatically using a cron scheduler. Scheduling allows you to run your builds at different intervals to gain more insights and identify failure patterns and root causes. These insights can help in fixing your tests. If your CI allows it, you can enable alerts for sudden increases in test flakiness.

Talking about CI, Semaphore CI offers flaky test detection to manage unreliable tests in your entire test suite.

To initialize flaky test detection, find your project in Semaphore and:

  • Select the Flaky tests tab.
  • Click Initialize.

Semaphore recently announced the open beta of their Flaky Tests Dashboard. It is a specialized tool engineered to combat the unpredictability of flaky tests in your CI/CD pipeline.

Environment ConsistencyWhen dealing with flakiness in your tests, you should ensure consistency in test environments. By using containerization or virtualization, you can replicate the environment accurately and resolve environment-specific issues promptly. Since we are talking about consistency in a test environment, you can use Testcontainers, a testing library, to deal with environmental disparity and gain confidence in your tests.

You also must admit that modern software development is becoming complex, and your tech stack typically consists of various tools and technologies. At a minimum, there will be a messaging queue, a reverse proxy, and multiple databases. Now, for testing, instead of relying on mock services or in-memory implementations of these tools, you can use Testcontainers, which spins up a Docker container for that dependency and gives you consistent results.

Configure Automatic RetriesThe most common approach a developer can take to resolve a flaky test is retrying it automatically. This doesn’t seem like a fix because running it repeatedly means you are just trying your luck and hoping it may pass. The benefit of this approach is that you don’t have to go through a manual process of identifying and re-running the failed test. Junit5, a testing framework for Java, provides an annotation @RepeatedTest for scenarios like dealing with a flaky test.

public class MyRepeatedTest { @RepeatedTest(3) // Run the test method 3 times void repeatedTest() { // Add your logic }} ConclusionIn conclusion, flaky tests can significantly challenge software development teams, sabotaging the reliability of testing processes and slowing development and release cycles. Nevertheless, by enforcing best practices such as regular test maintenance, isolating and making tests independent, and implementing timeout strategies, teams can lower the number of flaky tests and ensure the quality and reliability of their testing suite. By investing in effective testing strategies, we can help teams deliver high-quality software that meets user requirements and expectations.

The post Best Practices for Identifying and Mitigating Flaky Tests appeared first on Semaphore.

View Details

Flaky tests undermine the reliability of automated testing processes, leading to potential delays in developing workflows and releases. Such inconsistencies can reduce confidence in testing frameworks, absorb crucial time meant for troubleshooting, and divert attention from genuine problems. Within the framework of CI/CD, where swift and dependable rollout of software is crucial.

This article delves into strategies for identifying and overcoming flakiness in Jest tests. By understanding the root causes and applying best practices, developers can enhance the stability and reliability of their test suites, ensuring that automated testing remains a good way of ensuring quality assurance in their software development lifecycle.

Overview of Jest as a Testing ToolJest, made by Facebook, is a JavaScript Testing Framework that’s all about simplicity. It’s great for React projects but also works well with other JavaScript setups. Its zero-configuration setup, instant feedback, and built-in coverage tool make Jest popular among developers.

Flaky tests are tests that produce inconsistent outcomes either passing or failing, without changes in the code. They are a significant pain point in software development, leading to mistrust in testing suites, wasted developer time, and potentially, the overlooking of genuine bugs. In Continuous Integration/Continuous Deployment (CI/CD) pipelines, flaky tests can cause unnecessary delays in deployments, affecting the overall software quality and delivery speed.

Flaky tests in Jest, as in other environments, often stem from non-deterministic behaviors within the tests or the code being tested. Examples include reliance on external services, timing issues, and improper handling of asynchronous operations.

What causes flaky tests when testing with jest?Several things can cause flakiness in your tests. Let’s look at some of them with some examples.

Mocking the wrong selectorAn example of this would be mocking the useSelector in Redux-React component. This may affect the way that the component is rendered and causes flakiness.

The wrong way:

import { render, screen } from "@testing-library/react";import MyComponent from "../MyComponent";import store from "../../../path-to-AppStore.ts";// Globally mocking useSelector hookjest.mock("react-redux", () => ({ ...jest.requireActual("react-redux"), useSelector: jest.fn().mockReturnValue("Mock-Greeting"),}));test("displays the mock greeting label", () => { render( <Provider store={store}> <MyComponent /> </Provider> ); const greetingLabel = screen.getByText("Mock-Greeting"); expect(greetingLabel).toBeInTheDocument();}); The first example demonstrates a common mistake when trying to mock Redux’s useSelector hook. It replaces the useSelector hook globally for all tests in the file with a mock function that always returns a fixed value (“Mock-Greeting”). The mock is hard-coded to return a specific value, reducing the test’s flexibility to handle different scenarios or state shapes.

The correct way:

// Correct approach using a mock storeimport { render, screen } from "@testing-library/react";import { Provider } from "react-redux";import configureMockStore from "redux-mock-store";import MyComponent from "../MyComponent";const mockStore = configureMockStore();const store = mockStore({ greeting: "Mock-Greeting", // Adjust this initial state to match your actual state structure});test("displays the mock greeting label", () => { render( <Provider store={store}> <MyComponent /> </Provider> ); const greetingLabel = screen.getByText("Mock-Greeting"); expect(greetingLabel).toBeInTheDocument();}); Here are the key differences:

  1. Isolation of Mocks: In the correct approach, the mock is scoped to the test by creating a mock store with the desired state. This prevents the mock from affecting other tests and allows for more precise control over the test environment.
  2. Flexibility: The mock store can be customized for each test, allowing you to simulate different states and scenarios easily. This is more flexible than a global mock that returns the same value for all tests.
  3. Realistic Testing Environment: By using a mock store that mimics the Redux store’s behavior, the test more accurately reflects how the component interacts with Redux in a real application. This leads to more reliable and meaningful test results.

Hard coding wait timersWe can simulate a scenario where the “Loading…” text for a component disappears after a fixed but arbitrary delay that may or may not be adequately waited for in the test. This approach introduces non-determinism, as the test’s success could depend on timing specifics that aren’t consistently handled.

The wrong way:

import { render, screen, fireEvent, waitFor } from "@testing-library/react";import Button from "../path-to-Button-component";test("Button text changes incorrectly", async () => { jest.useFakeTimers(); render(<Button initialText="Click Me" />); fireEvent.click(screen.getByRole("button")); jest.advanceTimersByTime(500); // Assumes operation takes 500ms // This might fail if the actual loading time is longer than 500ms expect(screen.getByRole("button")).toHaveTextContent("Clicked"); jest.useRealTimers();}); The wrong approach uses Jest’s fake timers to artificially advance time, assuming the duration of the asynchronous operation. This method can lead to flaky tests if the assumed duration doesn’t match the actual time required for the operation.

Correct way:

// Correct approachimport { render, screen, fireEvent, waitFor } from "@testing-library/react";import Button from "../path-to-Button-component";test("Button text changes correctly", async () => { render(<Button initialText="Click Me" />); fireEvent.click(screen.getByRole("button")); // Dynamically wait for the button text to change to "Clicked" await waitFor(() => expect(screen.getByRole("button")).toHaveTextContent("Clicked") );}); The correct approach uses waitFor from the React Testing Library to dynamically wait for the UI to update to the expected state. This method does not make assumptions about how long the asynchronous operation will take, making the test more reliable.

To ensure the test is not flaky and handles asynchronous behavior correctly, we should avoid using fixed timers and instead rely on Jest’s and Testing Library’s built-in mechanisms to wait for the expected conditions.

Wrong use of async in your Jest test.Stop using the jest async keyword In tests where no async event is happening.

This is wrong:

// Wrong approach:test("incorrectly attempts to fetch user data without waiting", async () => { fetchUserData("userId123").then((userData) => { // This assertion might not be reached before the test completes. expect(userData).toBeDefined(); }); // The test might end before the fetchUserData promise resolves.}); The wrong approach demonstrates an asynchronous test where the async keyword is used, but the test does not properly wait for the asynchronous operation (fetchUserData) to complete before making assertions. This can lead to flaky tests or false positives where tests pass without actually validating the expected conditions.

Issues with This Approach:* Premature Test Completion: The test may complete before the promise returned by fetchUserData is resolved, meaning the assertions inside the .then() may not execute. * Unreliable Test Outcomes: Since the test does not wait for the asynchronous operation to finish, it can lead to unreliable outcomes, potentially missing failures.

Correct way:

// Correct approach:test("correctly fetches user data", async () => { const userData = await fetchUserData("userId123"); expect(userData).toBeDefined(); expect(userData.name).toEqual("John Doe");}); The correct approach uses async and await together, ensuring that the test waits for the fetchUserData operation to complete before proceeding with the assertions. This pattern guarantees that the asynchronous operation’s result is available for evaluation, making the test outcomes reliable and accurate.

Advantages of This Approach:* Guaranteed Execution Order: When using await, the test will pause until the asynchronous operation finishes, ensuring that assertions are only executed afterward. * Reliable and Accurate Testing: This method offers a dependable way to test asynchronous operations by accurately waiting for completion, ensuring meaningful and correct assertions. * Simplicity and Clarity: Utilizing async/await makes tests easier to read and understand, clearly indicating asynchronous operations that must be awaited.

Problems with Non-deterministic Inputs/Outputs:Flakiness occurs when tests rely on inputs or outputs that can unpredictably vary between runs. This variability can arise from external data sources, random data generation, or any inconsistent form of input across tests.

Consider a simple function getRandomNumber that returns a random number between 1 and 10. Directly testing this function poses challenges due to its non-deterministic output.

Function implementation:

function getRandomNumber() { return Math.floor(Math.random() * 10) + 1;} Bad Code Example: Using random data within tests without controlling the randomness.

test("returns a number between 1 and 10", () => { const number = getRandomNumber(); expect(number).toBeGreaterThanOrEqual(1); expect(number).toBeLessThanOrEqual(10);}); The test uses Math.random() directly, leading to different inputs for each test run, making the test outcome unpredictable. While the above test checks if the number is within the expected range, it doesn’t control the randomness, making it impossible to test for specific values reliably.

Good Code Example: Mocking or seeding random values to ensure consistency across test runs.

test("getRandomNumber returns a predictable random number", () => { // Mock Math.random to always return 0.5 jest.spyOn(Math, "random").mockReturnValue(0.5); const number = getRandomNumber(); // Since Math.random is mocked to return 0.5, the output of getRandomNumber will be predictable expect(number).toBe(6); // (0.5 * 10) + 1 = 6 jest.spyOn(Math, "random").mockRestore();}); In the correct approach, Math.random is replaced with a mocked version that consistently returns a fixed value (e.g., 0.5). This action ensures predictability and testability of the getRandomNumber function’s output. By employing this technique, you can craft precise assertions regarding the function’s behavior, thereby guaranteeing deterministic and reliable tests. However, it’s crucial to restore the original behavior of Math.random after the test to prevent the mock from impacting subsequent tests.

Problems with Time-Based LogicTests that depend on time-based logic can become flaky if they rely on real-time delays or the system clock. These tests may pass or fail depending on the execution speed of the test environment.

Bad Code Example: Using actual delays in tests.

test("flaky test with real delay", (done) => { setTimeout(() => { expect(true).toBeTruthy(); done(); }, 1000); // Waits 1 second}); The test waits for a real-time delay using setTimeout, making the test slow and its success dependent on the environment’s timing.

Good Code Example: Using Jest’s fake timers to simulate time passing.

test("reliable test with fake timers", () => { jest.useFakeTimers(); setTimeout(() => { expect(true).toBeTruthy(); }, 1000); jest.runAllTimers(); // Simulates all timers running}); The test uses Jest’s fake timers to simulate the delay, ensuring the test runs quickly and its outcome is independent of the environment’s timing.

Problems with Race ConditionsRace conditions occur when the outcome of a test depends on the sequence or timing of uncontrollable events, such as API calls or database operations. Tests with race conditions can unpredictably pass or fail.

Bad Code Example: Testing asynchronous operations without properly handling the execution order.

test("flaky test due to race condition", () => { let value = 0; asyncOperation().then(() => (value = 1)); expect(value).toBe(1); // May fail if asyncOperation hasn't completed}); The test makes an assertion immediately after initiating an asynchronous operation, without waiting for it to complete. This can lead to the assertion being evaluated before the operation finishes, causing unpredictable test outcomes.

Good Code Example: Ensuring asynchronous operations complete before assertions.

test("reliable test avoiding race conditions", async () => { let value = 0; await asyncOperation(); value = 1; expect(value).toBe(1);}); The test awaits the completion of the asynchronous operation before making assertions, ensuring that the test outcome is deterministic and reflects the operation’s actual result.

Strategies for Identifying Flaky TestsTools like Jest’s --runInBand can help identify flaky tests by running tests sequentially, reducing the chances of tests interfering with each other. Additionally, custom scripts or third-party tools can automate the process of running tests multiple times to spot inconsistencies.

Isolating flaky tests involves running them under varied conditions to understand their behavior better. Techniques include altering the execution order of tests, mocking external dependencies, and using Jest’s timers to control timing-based logic.

Best Practices for Writing Reliable Tests Deterministic Inputs and Outputs: Ensure tests have predictable inputs and outputs. Use mocking and stubbing to isolate the test environment from external dependencies. * Proper Asynchronous Handling: Use Jest’s async testing features correctly to handle promises, async/await, and callbacks. * Avoid Timing-Based Logic: Where possible, avoid logic that depends on timing or delays. Use Jest’s fake timers to simulate timers. * Managing External Dependencies and StateMock external services and APIs to ensure tests do not rely on external factors. Reset the state before each test to prevent tests from affecting each other. * Addressing Timing Issues and Race Conditions*Utilize Jest’s fake timers to control JavaScript timers in your tests, eliminating flakiness caused by timing issues. Ensure proper synchronization of asynchronous operations to avoid race conditions.

Note: Regular review and maintenance of the test suite are crucial. Periodically audit tests for flakiness, optimize test code, and update tests alongside the codebase changes.

ConclusionDealing with flaky tests in Jest brings about best practices, thoughtful test design, and leveraging tools and methods to address non-deterministic behavior. Embracing these approaches allows developers to maintain the reliability of their Jest tests, thereby enhancing the quality and resilience of their software projects.

Resources* Intro to Flaky tests

The post How to Manage Flaky Tests in Jest appeared first on Semaphore.

View Details

Tired of a Cypress flaky test stopping your deployments? A test is labeled as “flaky” when it produces inconsistent results across different runs. It may pass on one occasion and fail on subsequent executions without clear cause.

Flaky tests represent a huge issue in CI/CD systems, as they lead to seemingly arbitrary pipeline failures. That is why you need to know how to avoid them!

In this guide, you will grasp the concept of flaky tests and delve into their root causes. Then, you will see some best practices to avoid writing flaky tests in Cypress.

Let’s dive in!

What Is a Flaky Test in Cypress?

“A test is considered to be flaky when it can pass and fail across multiple retry attempts without any code changes.

For example, a test is executed and fails, then the test is executed again, without any change to the code, but this time it passes.”

This is how the Cypress documentation defines a flaky test.

In general, a flaky test can be defined as a test that produces different results in different runs on the same commit SHA. To detect such a test, remember that flaky tests can pass on the branch but then fail after merging.

The consequences of flaky tests are particularly pronounced within a CI pipeline. Their unreliable nature leads to unpredictable failures during different deployment attempts for the same commit. To deal with them, you must configure the pipeline for retry on failure. That results in delays and confusion, as each deployment seems to be vulnerable to apparently arbitrary malfunctions.

Main Causes Behind a Flaky TestThe three main reasons for flaky behavior in a test are:

  • Uncontrollable environmental events: For example, assume that the application under test has unexpected slowdowns. This can lead to inconsistent results in your tests.
  • Race conditions: Simultaneous operations can result in unexpected behavior, especially on dynamic pages.
  • Bugs: Errors or anti-patterns in your test logic can contribute to test flakiness.

These factors can contribute individually or collectively to flakiness. Let’s now see some strategies to avoid Cypress flaky tests!

Best Practices to Avoid Writing Flaky Tests in CypressExplore some best practices endorsed by the official documentation to avoid flaky tests in Cypress.

Debug Your Test LocallyThe most straightforward way to avoid Cypress flaky tests is to try to prevent them. The idea is to carefully inspect all tests locally before committing them. That way, you can ensure that they are robust and work as expected.

Bear in mind that you need to get results that are comparable to those in the CI/CD pipeline. Thus, make sure that the local Cypress environment shares the same configuration and a similar environment as the CI pipeline.

After setting up the environment and configuring Cypress properly, you can launch it locally with:

npx cypress open This will open the Cypress App:

The left-hand side of the application contains the Command Log, a column where each test block is properly nested. If you click on a specific test block, the Cypress App will show all the commands executed within it as well as any commands executed in the before, beforeEach, afterEach, and after hooks.

On the right, there is the application under test running in the selected browser. In this section, you have access to all the features the browser makes available to you, such as the DevTools.

Since a flaky test may or may not fail by definition, you should run each test locally more than once. When a test fails, Cypress will produce a detailed error as follows:

This contains the following information:

  1. Error name: The type of the error (e.g., AssertionError, CypressError, etc.)
  2. Error message: Tells you what went wrong with a text description.
  3. Learn more: A link to a relevant Cypress documentation page that some errors add to their message.
  4. Code frame file: Shows the file, line number, and column number of the cause of the error. It corresponds to what is highlighted in the code snippet below. Clicking on this link will open the file in your integrated IDE.
  5. Code frame: A snippet of code where the failure occurred.
  6. View stack trace: A dropdown to see the entire error stack trace.
  7. Print to console: A button to print the full error in the DevTools console.

As a first step to debug a Cypress flaky test, you want to enable event logging. To do so, execute the command below in the console of the browser on the right:

localStorage.debug = 'cypress:*' Then, turn on the “Verbose” mode and reload the page:

When running a test, you will now see all Cypress event logs in the console as in the following example:

This will help you keep track of what is going on.

Next, call the debug() method on the instruction that caused the error:

it('successfully loads', () => { cy.visit('http://localhost:5000') // note the .debug() method call cy.get('.submit-button').click().debug()}) This defines a debugging breakpoint by pausing the test execution. It also logs the current system’s state in the console. Under the hood, debug() uses the JavaScritpt debugger instruction. Therefore, you need to have your Developer Tools open for breakpoint to hit.

If you instead prefer to programmatically pause the test and inspect the application manually in the DevTools, use the cy.pause()instruction:

it('successfully loads', () => { cy.visit('http://localhost:5000') // interrupt the test execution cy.pause() cy.get('.submit-button').click()}) This allows you to inspect the DOM, the network, and the local storage to make sure everything works as expected. Note that pause() can be also be chained off other cy methods. To resume execution, press the “play” button in the Command Log.

After debugging your tests, run them with the same command you would use in the CI/CD pipeline:

npx cypress run As opposed to running tests with cypress open, Cypress will now automatically capture screenshots when a failure happens. If that is not enough for debugging, you can take a manual screenshot with the cy.screenshot() command.

To enable video recordings of test runs on cypress run, set the video option to true in your Cypress configuration file:

const { defineConfig } = require('cypress')module.exports = defineConfig({ // enable video recording of your test runs video: true,}) Cypress will record videos on both successful and failed tests. As of this writing, this feature only works on supported Chromium-based browsers.

Screenshots and videos make it easier to understand what happens when executing tests in headless mode and in the same configurations as your CI/CD setup.

Select Elements Via Custom HTML AttributesModern JavaScript applications are usually highly dynamic and mutable. Their state and DOM change continuously over time based on user interaction. Using selection strategies for HTML nodes that are too tied to the DOM or application state leads to Cypress flaky tests.

As a best practice to avoid flakiness, the Cypress documentation recommends writing selectors that are resilient to changes. In particular, the tips to keep in mind are:

  1. Do not target elements based on their HTML tag or CSS attributes like id or class.
  2. Try to avoid targeting elements based on their text.
  3. Add custom data-* attributes to the HTML element in your application to make it easier to target them.

Consider the HTML snippet below:

<button id="subscribe" class="btn btn-primary" name="subscription" role="button" data-cy="subscribe-btn"> Subscribe</button> Now, analyze different node selection strategies:

| Selector | Recommended | Notes | | --- | --- | --- | | cy.get('button') | Never | Too generic. | | cy.get('.btn.btn-primary') | Never | Coupled with styling, which is highly subject to change. | | cy.get('#subscribe') | Sparingly | Still coupled to styling or JS event listeners. | | cy.get('[name="subscription"]') | Sparingly | Coupled with the name attribute, which has HTML semantics. | | cy.contains('Subscribe') | Depends | Still coupled to text content, which may change dynamically. | | cy.get('[data-cy="subscribe-btn"]') | Always | Isolated from all changes. |

In short, Cypress recommends the selection of nodes through custom-defined HTML attributes like data-cy.

Configure Automatic RetriesBy default, Cypress does not retry tests when they fail. This is bad (at least when running locally) because retrying tests is one of the best ways to identify flaky behavior. If a test fails and then passes in a new attempt, it is likely to be a Cypress flaky test.

Luckily, Cypress supports test retries. In detail, you can configure a test to have X number of retry attempts on failure. When each test is run again, the beforeEach and afterEach hooks will also be re-run.

You can configure test retries with the following options:

  • runMode: Specifies the number of test retries when running tests with cypress run. Default value: 0.
  • openMode Specifies the number of test retries when running tests with cypress open. Default value: 0.

Set them in the retries object in cypress.config.js as follows:

const { defineConfig } = require('cypress')module.exports = defineConfig({ retries: { // configure retry attempts for `cypress run` runMode: 2, // configure retry attempts for `cypress open` openMode: 1 }}) In this case, Cypress will retry all tests run with cypress run up to 2 additional times (for a total of 3 attempts) before marking them as failed.

If you want to configure retry attempts on a specific test, you can set that by using a custom test configuration:

describe('User sign-up and login', () => { it( 'allows user to login', // custom retry configuration { retries: { runMode: 2, openMode: 1, }, }, () => { // ... } )}) To configure retry attempts for a suite of tests, specify the retries object in the suite configuration:

describe( 'User stats', // custom retry configuration { retries: { runMode: 2, openMode: 1, }, }, () => { it('allows a user to view their stats', () => { // ... }) it('allows a user to see the global rankings', () => { // ... }) // ... }) Normally, test retries stop on the first passing attempt. The test result is then marked as “passing,” regardless of the number of previous failed attempts. As of Cypress 13.4, you have access to experimental flake detection features to specify advanced retry strategies on flaky tests. Learn more in the official documentation.

Do Not Use cy.wait() for Fixed-Time Waitscy.wait() is a special Cypress command that can be used to wait for a given number of milliseconds:

cy.wait(5000) // wait for 5 seconds Using this function is an anti-pattern that leads to flaky results. The reason is that you cannot know in advance what is the right time to wait for. This depends on aspects you do not have control over, such as server or network speed.

As a general rule, keep in mind that you almost never need to wait for an arbitrary period of time. There are always better ways to achieve that behavior in Cypress. In most scenarios, waiting is unnecessary.

For example, you may be tempted to write something like this:

cy.request('http://localhost:8080/api/v1/users')// wait for the server to respondcy.wait(5000)// other instructions... The cy.request() command does not resolve until it receives a response from the server. In the above snippet, the explicit wait only adds 5 seconds after cy.request() has already resolved.

Similarly, cy.visit() resolves once the page fires its load event. That occurs when the browser has retrieved and loaded the JavaScript, CSS stylesheets, and HTML of the page.

What about dynamic interactions? In this case, you may want to wait for a specific operation to complete as in the example below:

// click the "Load More" buttoncy.get('[data-cy="load-more"]').click()// wait for data to be loaded on the pagecy.wait(4000)// verify that the page now contains 20 productscy.get('[data-cy="product"]').should('have.length', 20) Again, that cy.wait() is not required. Whenever a Cypress command has an assertion, it does not resolve until its associated assertions pass.

That does not mean Cypress commands will run forever waiting for specific conditions to occur. On the contrary, almost any command will timeout after some time, which leads to the next section.

Configure Timeouts ProperlyTimeouts are a core concept in Cypress, and these are the most important ones you should know:

  • defaultCommandTimeout: Time to wait until most DOM-based action commands (e.g., click() and similar methods) are considered timed out. Default value: 4000 (4 seconds).
  • pageLoadTimeout: Time to wait for page transition events or cy.visit(), cy.go(), cy.reload() commands to fire their page load events. Default value: 60000 (60 seconds).
  • responseTimeout: Time to wait until a cy.request(), cy.wait(), cy.fixture(), cy.getCookie(), cy.getCookies(), cy.setCookie(), cy.clearCookie(), cy.clearCookies(), or cy.screenshot() command completes. Default value: 30000 (30 seconds).
  • execTimeout: Time to wait for a system command to finish executing during a cy.exec() command. Default value: 60000(60 seconds).
  • taskTimeout: Time to wait for a task to finish executing during a cy.task() command. Default value: 60000 (60 seconds).

All Cypress timeouts have a value in milliseconds that can be configured globally in cypress.config.js as follows:

const { defineConfig } = require('cypress')module.exports = defineConfig({ defaultCommandTimeout: 10000, // 10 seconds pageLoadTimeout: 120000, // 2 minutes // ...}) Alternatively, you can configure them locally in your test files with Cypress.config():

Cypress.config('defaultCommandTimeout', 10000) You can also modify the timeout of a particular Cypress command with the timeout option:

cy.get('[data-cy="load-more"]', { timeout: 10000 }) .should('be.visible') Cypress will now wait up to 10 seconds for the button to exist in the DOM and be visible. Note that the timeout option does not work on assertion methods.

Bad timeout values are one of the core reasons behind Cypress flaky tests, especially when an unexpected slowdown occurs.

Do Not Rely on Conditional TestingThe Cypress documentation defines conditional testing as a leading cause of flakiness. If you are not familiar with this concept, conditional testing refers to this pattern:

“If X, then Y, else Z”

While this pattern is common in traditional development, it should not be used when writing E2E tests. Why? Because the DOM and the state of a webpage is highly mutable!

You may add an if statement to check for a specific condition before performing an action in your test:

cy.get('button').then(($btn) => { if ($btn.hasClass('active')) { // do something if it is active... } else { // do something else... }}) The problem is that the DOM is so dynamic that you have no guarantee that by the time the test executes the chosen if branch, the page has not changed.

You should use conditional tests on the DOM only if you are 100% sure that the state has settled and that there is no way it can change. In any other circumstance, if you rely on the state of the DOM for conditional testing, you may end up writing Cypress flaky tests.

Thus, limit conditional testing to server-side rendered applications without JavaScript or pages with a static DOM. If you cannot guarantee that the DOM is stable, you should follow a more deterministic approach to testing as described in the documentation.

Consider the Flaky Test Management Features of Cypress CloudCypress Cloud is an enterprise-ready online platform that integrates with Cypress. This paid service extends your test suite with extra functionality, including features for flaky test management like:

  • Flake detection: To detect and monitor flaky tests as they occur. It also enables you to assess their severity and assign them a priority level.
  • Flagging flaky tests: Test runs will automatically flag flaky tests. You will also have a special option to filter them out via the “Flaky” filter.
  • Flaky test analytics: A page with statistics and graphs highlighting the flake status within your project. It shows the number of flake tests over time, the overall flake level of the entire project, the number of flake tests grouped by their severity, and more. You can also access the historical log of the last flake runs, the most common errors among test case runs, the changelog of related test cases, and more.
  • Flake alerting: Integration with source control tools such as GitHub, GitLab, BitBucket, and others to send messages on Slack and Microsoft Teams when a Cypress flaky test is detected.

Unfortunately, none of these features are available in the free Cypress Cloud plan. However, Semaphore users can enable their flaky test dashboard to automatically flag flaky tests in their suite as they run their CI pipelines.

How to Fix a Flaky Test in CypressThe best practices outlined above help minimize Cypress flaky tests. However, you cannot really eliminate them altogether. What approach should you take when discovering a flaky test in Cypress? A good strategy involves following these three steps:

  1. Indentify the root cause: Run the flaky test several times locally and debug it to understand why it produces inconsistent results.
  2. Implement a fix: Address the cause of flakiness by updating the test logic. Execute the test locally many times and under the same conditions that lead to flaky results. Ensure that it now works as desired.
  3. Deploy the updated test: Check that the test now generates the expected results in the CI/CD pipeline.

For more information, read our guide on how to fix flaky tests. Take also a look at the Cypress blog, as it features several in-depth articles on how to address flakiness.

ConclusionIn this article, you saw what a flaky test is, what consequences it has in your CI/CD process, and why it can occur. Then, you explored some Cypress best practices to address the most relevant causes of flaky tests. You can now write tests that are robust and produce consistent results all the time. Even if you cannot eliminate flakiness altogether, you can reduce it to the bare minimum. Protect your CI/CD pipeline from unpredictable failures!

Learn more about flaky tests:

  • How to Avoid Flaky Tests in Playwright
  • Handling Flaky Tests in LLM-powered Applications
  • How to Avoid Flaky Tests in Selenium

The post How to Avoid Flaky Tests in Cypress appeared first on Semaphore.

View Details

In the context of React, testing is a non-negotiable process to maintain code quality and a smooth user experience.

However, there’s one frustrating bad news that is commonly faced when running tests in React. And that is flaky tests.

In the simplest of words, flaky tests are tests that seem to pass most of the time but fail sometimes, all without changes to the code or test — just for no reason.

In this guide, we’ll focus on flaky tests, particularly in React, the various causes, how to detect them, how to fix them, and the efficient tools that are used.

Understanding Flaky Tests in ReactFlaky tests, especially in UI testing, are a common pain point for developers. It is almost unavoidable that even Google reports that around 14% of their tests were flaky.

Here’s a short scenario to further understand flaky tests in React:

So you wrote a test for a React component that displays a button that when clicked, sends a notification to the user. Then you run the test and it passes — all green. The next day, you run the test again, it passes — all green. Then, for the sake of finalizing it, you ran the test again one last time — red, it failed.

Now it shouldn’t have failed because it is the same test and the same component; nothing changed, but it failed suddenly. So as expected, you ran the test again — it failed, ran it again — passed; ran it again — failed.

Now, what exactly causes flaky tests in React?

Common Causes of Flaky Tests in ReactLet’s clear up something before we proceed. The exact cause of flaky tests in React could be anything; it varies. This is mostly due to the dynamic nature of React components and how they interact.

However, there are some common causes that you can keep an eye out for that could be the culprit. Let’s see them in more detail.

External DependenciesAlmost every React application interacts with APIs, databases, or third-party services, and as expected, tests also rely on them.

Now, for example, you have a test that checks if a list of products is displayed after fetching data from an API. However, if the API response is slow or down, the test might fail even though the code is working correctly. This flakiness happens because the test relies on an external factor that is mostly out of your control.

For example, here is a component that gets a list of products from an API and displays them:

// imports ...export function ProductsList() { const [products, setProducts] = useState([]); useEffect(() => { const fetchProducts = async () => { try { const response = await fetch("https://api.example.com/products"); const data = await response.json(); setProducts(data); } catch (error) { console.log(error); } }; fetchProducts(); }, []); return ( {products.map((product) => ( * {product.name} ))} );} Now the test might look something like this:

import "@testing-library/jest-dom";import { render, screen, waitFor } from "@testing-library/react";describe("ProductsList", () => { test("should render a list of products", async () => { render(); await waitFor(() => { expect(screen.getByText("Product 1")).toBeInTheDocument(); expect(screen.getByText("Product 2")).toBeInTheDocument(); }); });}); This test might work fine; however, it is flaky because it relies on an external API, which as usual comes with a lot of uncertainties like network delays or server issues. This can be fixed using mocks (more on that later).

Timing IssuesWho knows if a component will take longer than the expected time? A test can rely on a process that takes unpredictable time to complete; a core process in this category is animations and transitions.

If there’s a test that checks a specific UI element before or after an animation runs, rest assured that a flaky test showing up won’t be surprising.

Asynchronous OperationsIn React, a lot of tasks don’t happen instantly, like waiting for user input, UI updates, or fetching data from servers. If your tests don’t wait for these operations to complete before making assertions, they will return failed.

We know in React that when a component’s state changes, the virtual DOM updates, and then the actual UI is updated asynchronously. So having tests that assert the UI state immediately after a state update might be flaky because the UI hasn’t been updated yet. Here is an instance:

``` // imports here ...function Counter() { const [count, setCount] = useState(0); const handleClick = () => setCount((count) => count + 1); return ( <> {count}

  Increment      );}describe("Counter", () => {  it("should update count after click", () => {    render();    fireEvent.click(screen.getByText("Increment"));    expect(screen.getByText("1")).toBeInTheDocument();  });});

`` This test seems straightforward, but flakiness comes in when the test runs the assertion before the UI has reflected the change, so it is recommended to use thewaitFor` utility:

describe("Counter", () => { it("should update count after click", async () => { render(); fireEvent.click(screen.getByText("Increment")); // Wait for the UI to update await waitFor(() => { expect(screen.getByText("1")).toBeInTheDocument(); }); });}); Leaky StateThis happens when tests modify the global state or have side effects that weren’t accounted for. Thus, these changes can interfere with the next test that would run, leading to unexpected failures.

This is mostly common where multiple React components rely on a state for rendering. You have Test A, which sets a state variable if a user is logged in. Now, if Test B relies on this state and doesn’t reset it before it runs, it might fail because it expects the user to be logged out.

Flawed TestsProbably due to deadlines, rushing tests isn’t unheard of. Most of us have been there, we want to see greens quickly and move on to other things. But more often than not, tests written in haste often run on assumptions, and that leads to flakiness.

Consider these components:

``` export function TestA() { useEffect(() => { localStorage.setItem("user", "minato"); }, []); return Test A Sets user in localStorage

;}export function TestB() { return Test B: Reads user from localStorage

;} `` Here isTestA` test file:

test("TestA sets user in localStorage", () => { render(); expect(localStorage.getItem("user")).toBe("minato");}); Here is TestB test file:

test("TestB reads user from localStorage", () => { render(); expect(localStorage.getItem("user")).toBeNull();}); TestA sets a value in localStorage using a side effect in useEffect. The side effect isn’t cleaned up, potentially interfering with subsequent tests.

TestB expects localStorage.getItem('user') to be null but might fail due to the leak. You can use beforeEach and afterEach to always clean up side effects.

Impact of Flaky Tests on Development Workflow and Product QualityLet’s say you just finished a new search feature in your React application. You ran it through your CI/CD pipeline, and all tests passed. You ran it one more time and all passed, then you merged the code to the main branch, and the build failed! Then you go through the code, nothing seems to be wrong, you run the tests again, and…they fail.

What just happened in this scenario is how flaky tests can create a false sense of code security. Another known impact is decreased trust in testing. When tests begin to fail at random, it is frustrating, and with time, developers tend to start ignoring flaky tests and tagging them as “expected failures.” This over time leads to buggy UIs.

Avoiding these impacts are important and doing so early on comes with some benefits:

  • It saves time and money.
  • It ensures a stable user experience at all times.
  • It allows for a smooth and reliable CI/CD process.

Detecting Flaky Tests in ReactThere are quite several known ways developers use to detect flaky tests in React, some are manual, while others are automatic. Let’s check them out.

Review Test CodesAlways check out the code in the tests, especially those involving async processes, they are majorly involved in the flakiness of React tests. Also, check if the tests clean up either before a new one begins or after an old one ends.

Jest provides two hooks suitable for this beforeEach and afterEach:

// functions and logic here ...describe("items in correct group", () => { beforeEach(() => getAnimalData()); afterEach(() => clearAnimalData()); test("animals in right category", () => { expect(isAnimalInCategory("cat", "mammals")).toBeTruthy(); });}); Also, when reviewing test codes, can the tests run independently and perfectly without relying on external states or global variables? This is because if a test relies on external data that can’t be controlled, randomness and unpredictability set in and thereby introduce flakiness.

Analyzing Error HandlingA common source of flakiness in React is inadequate error handling within components. This is simply because uncaught errors can alter execution flow, which can lead to failing tests that may not even be related to the current test running.

For instance, is the React Testing Library implemented thoughtfully? Are potential errors accounted for and handled effectively? All these count because if an error is not accounted for, it could cause tests to fail at random.

Stable Testing EnvironmentWhen running tests in React, the consistency of the environment is paramount. Let’s say you run some tests in your local environment, and the tests all pass, but when you run them on the CI/CD pipeline, some fail. Then environmental change could be the cause.

Essentially, all dependencies, tools, and configurations should remain identical for each test run. Just a slight hardware configuration in a tool can lead to a flaky test flaring up.

Logging for InsightsTo find a reason for anything failing in software development — React included, logging is among the top methods used.

A simple console.log() can do wonders in locating the cause of a particular flaky test. Just placing log statements around your test suite can show detailed tracking of how the test execution flows, and with that, identifying patterns that lead to the test failing would be much easier.

To make things easier, the React Testing Library provides a method screen.debug that helps in logging elements or the whole rendered document.

function Card({ title }) { return {title};}describe("Card rendering", () => { test("renders title", () => { render(); screen.debug(); // renders the document expect(screen.getByText("Flight to Mars")).toBeInTheDocument(); //renders only the card component screen.debug(screen.getByText("Flight to Mars")); });}); The Order of Elements MattersLet’s use an example to explain this, a component renders a list of products, and you wrote a test that expects the last item to be a drink. The test might pass for now, but can you be certain that the last item will always be a drink? The product data structure may change (e.g. sorting algorithm updates or code refactoring). So never assume that data will come exactly as you expect. Instead, use unique IDs to target specific elements within the UI when testing and overall.

The use of CI/CD PipelinesIt is inefficient to run tests manually; sure when running it for small tests, it might be no big deal. But it becomes impractical for large React codebases or frequent test runs, let’s say 93 times — that’s a lot.

Now, this is where automation comes in, and CI/CDs are the best at that. You can easily integrate React tests with a CI/CD pipeline to automate the process. Many CI/CD platforms, like Semaphore, have built-in features to easily detect and report flaky tests.

They automatically run your tests whenever code changes are pushed to a repository and notify developers of flaky tests that happened during automated testing. These platforms can rerun failed tests multiple times to confirm the flakiness before marking the tests as failed (however, this would add to your bill).

Preventing Flaky Tests in ReactUnderstand that, more often than not, flaky tests tend to turn out to be potential bugs when ignored. So preventing or fixing these tests as soon as possible is paramount, at least for UI stability.

Use CI/CD EarlyThis automated technology is one of the best options out there for preventing and even detecting flaky tests early in the development cycle. You can set up a CI/CD pipeline like SemaphoreCI and configure it to trigger automatic test execution. This would give you detailed reports on test failures, the stack traces, and even logs of how, when, and why a flaky test occurred.

Structure Your Tests WellThis is simple, a well-structured test is easier to maintain, because as a codebase grows, so does the number of tests and the higher chances of getting flaky tests.

  • Tests should be independent to prevent a chain reaction whereby because a test fails randomly, other tests after it might also show the same behavior.
  • Tests should have meaningful names.
  • A test should set up its own component instances when it runs.
  • Consider having tests that run to check if dependencies or data are available or functions properly before running core tests.

Minimize Fixed Wait TimesFixed wait times (e.g. setTimeout) should be used as minimally as possible, as they are unpredictable, especially during UI changes or animations. Instead, use events, async/await, or promises to deal with these situations; it is much more efficient.

Let’s say a test clicks a button to open a modal that runs for 500ms. Instead of using a fixed wait of 500ms in the testing, you should use waitFor that would run the test assertion after the timer ends.

Here is an example that illustrates this:

function Modal() { const [isOpen, setIsOpen] = useState(false); const handleOpen = () => { setTimeout(() => setIsOpen(true), 500); }; return ( <> {isOpen && This is the modal} Open Modal );} Let’s create a test that would be problematic due to fixed wait times:

test("Modal opens", async () => { render(); fireEvent.click(screen.getByText("Open Modal")); setTimeout(() => { expect(screen.getByTestId("modal")).toBeInTheDocument(); }, 500);}); This would show as the test passed, however, the assertion didn’t run before the test ended. We can fix it by using async/awaitand waitFor

test("Modal opens", async () => { render(); fireEvent.click(screen.getByText("Open Modal")); await waitFor(() => expect(screen.getByTestId("modal")).toBeInTheDocument());}); Now the test would wait for the modal to open before checking its presence through an assertion.

Be Mindful of Dynamic DataPay attention to random or unpredictable data that can change uncontrollably during multiple test runs. An example is UUIDs, they are really good for react optimization, however, during testing, because of their randomly generated nature, these UUIDs would differ in multiple test runs, which leads to flaky results. Instead, a predictable pattern can be used, like incrementing a counter (just for testing purposes).

Other dynamic data are user inputs and dates.

Use of MocksMocks are best for replacing external dependencies with more of a dummy-controlled version that the components can use during testing. This gives the test more predictable behavior, and one doesn’t have to deal with the inconsistent nuances of external dependencies.

Let’s revisit the ProductsList code example we used in External Dependencies as a cause of flaky tests in React. Here is how a mock can help:

const mockProducts = [ { id: 1, name: "Product 1" }, { id: 2, name: "Product 2" },];describe("ProductsList", () => { test("should render a list of products", async () => { global.fetch = jest.fn().mockResolvedValue({ json: jest.fn().mockResolvedValue(mockProducts), }); render(); await waitFor(() => { expect(global.fetch).toHaveBeenCalledWith( "https://api.example.com/products" ); }); await waitFor(() => { expect(screen.getByText("Product 1")).toBeInTheDocument(); }); await waitFor(() => { expect(screen.getByText("Product 1")).toBeInTheDocument(); }); });}); In this test, mockProducts array contains dummy data that’d be used in place of the original fetched API data. Then jest.fn() is used to mock the global fetch function so we can control its behavior within the test.

The mock is then configured to be a successful fetch response (mockResolvedValue) that returns the mockProducts array. After which, we run the usual assertions.

With this in place, we can be certain that the test focuses mainly on its code logic, isolating it from external factors.

Fix flaky tests as soon as they show upThis is because if a test shows up all of a sudden as flaky and you put a tag on it—you’ll fix it later. However, when that time comes and you run the tests, it may continue to pass every time, and the flakiness might not show up.

It doesn’t mean it is fixed; it could mean that the particular reason you got a flaky test initially was due to the time of day. Now you’d have to wait for the flaky test to show up again, or you risk pushing the code to production and hoping all works out well (not recommended).

Writing Stable TestsHere are some good practices for writing stable tests in React:

  • Each test should focus on the behavior of a specific React component in isolation
  • Start with smaller tests, it makes the tests easier to understand.
  • Use beforeEach and afterEach methods to ensure each test starts with a clean slate
  • waitFor and act are good options for handling async operations.
  • Write synchronous tests unless the functionality explicitly involves asynchronous operations.
  • If your testing tool has support for snapshot testing, use it. It makes things easier.
  • Don’t mindlessly kick off flaky tests, instead, you can put a flag on it and fix it later. However, the faster you fix it, the better.
  • Document your tests to explain what they are testing and why.

ConclusionReact, being a UI library, has its own fair share of challenges when it comes to testing. However, we all learn from past mistakes, so if you aren’t getting the hang of solving the flaky tests you are facing at the moment. Just know it is normal, and with time, the more flaky tests you encounter and fix, the less your tests will become flaky and get overall better.

The post Flaky Tests In React: Detection, Prevention and Tools appeared first on Semaphore.

View Details

The ever-evolving landscape of software development demands constant adaptation and modernization. Legacy codebases, however, hold a vital place within many organizations, serving as the backbone of essential functionalities. While their longevity ensures stability, their age often presents challenges, and one particularly disruptive issue is the presence of flaky tests.

These tests, prone to intermittent failures without code changes, wreak havoc on development workflows. Imagine the frustration of a seemingly green build suddenly turning red on your next run, casting doubt on the validity of your changes and hampering progress. This uncertainty is only amplified within legacy codebases, where inherent complexities pose unique hurdles in addressing flakiness. Here’s a concise diagram to visually depict our discussion:

This article delves into the critical issue of flaky tests within legacy codebases. We’ll begin by demystifying the term and exploring its detrimental effects on development efficiency and code quality. Then, we’ll delve into the specific challenges posed by older code structures and dependencies. Finally, we’ll equip you with a roadmap of practical solutions, empowering you to tackle these roadblocks and restore trust in your test suite.

Technical Challenges & SolutionsBeyond the process hurdles, flaky tests often expose underlying technical issues within the legacy codebase itself. In this section, we’ll dissect these technical challenges and explore solutions to ensure your tests are reliable and effective.

ChallengesThis section discusses the technical challenges posed by flaky tests in legacy codebases.

Difficulty identifying root causes due to complex code and dependencies: Legacy codebases often evolve, accumulating layers of complexity and dependencies. This tangled web makes it challenging to pinpoint the root causes of flaky tests. Untangling the intricacies of legacy code to isolate issues requires a deep understanding of the system’s architecture and historical context.

Integration issues with existing testing frameworks: Legacy codebases may rely on outdated or incompatible testing frameworks, exacerbating the problem of flakiness. Integrating modern testing tools and practices into the existing infrastructure can be met with resistance and compatibility issues, hindering efforts to address flaky tests effectively.

Limited observability and debugging capabilities: Legacy systems frequently lack robust observability and debugging capabilities, making it arduous to diagnose and troubleshoot flaky tests. Without comprehensive logging, monitoring, and debugging tools, developers may struggle to gain insights into test failures and identify patterns of flakiness.

Potential impact on existing functionality during test refactoring: Refactoring tests to improve reliability can inadvertently disrupt existing functionality in legacy codebases. The interconnected nature of legacy systems means that modifications to one part of the codebase can have unintended consequences elsewhere. Balancing the need to refactor tests with the risk of introducing regressions requires careful planning and testing strategies.

SolutionsIn this section, we’ll discuss solutions for overcoming the technical challenges encountered in legacy codebases with flaky tests.

Utilizing modern testing tools and frameworks designed for flaky test detection and mitigation: Adopting advanced testing frameworks like JUnit with the @Flaky annotation (Java) or pytest with the --rerun flag (Python) can significantly improve the reliability of testing processes in legacy codebases. These frameworks often offer features like test retries, assertion retries, and statistical analysis to identify and mitigate flakiness effectively. Visual testing tools like Applitools or Cypress can also be valuable for detecting UI inconsistencies that might contribute to flakiness in web applications.

Implementing dependency management strategies to isolate tests and identify external factors: Implementing robust dependency management strategies can help isolate tests from external factors that contribute to flakiness in legacy codebases. By managing dependencies carefully and minimizing external influences on test execution, developers can create a more stable and predictable testing environment, reducing the likelihood of flaky tests.

Leveraging logging and monitoring tools for better observability and debugging: Integrating logging and monitoring tools into the testing infrastructure provides developers with valuable insights into test execution and failure patterns. By capturing detailed logs and metrics during test runs, developers can diagnose flaky tests more effectively and identify underlying issues that contribute to instability in legacy codebases.

Refactoring tests incrementally with clear documentation and version control: Refactoring tests incrementally allows developers to improve test reliability gradually without introducing disruptive changes to the existing codebase. By documenting changes thoroughly and using version control systems to track modifications, developers can ensure transparency and accountability throughout the refactoring process, minimizing the risk of unintended consequences on existing functionality.

Process Challenges & SolutionsBeyond the technical hurdles, flaky tests introduce complexities in our development workflow. This section will unveil the challenges we face in managing, tracking, and efficiently resolving these issues. We’ll then equip you with solutions to streamline the process and conquer these flaky foes.

ChallengesThis section comprehensively outlines the process challenges associated with flaky tests in legacy codebases.

Lack of ownership or accountability for legacy tests: In many organizations, legacy tests may lack clear ownership or accountability, leading to neglect and inconsistency in maintenance efforts. Without designated individuals or teams responsible for managing and improving legacy tests, issues such as flakiness may persist indefinitely.

Resistance to change from developers familiar with the existing codebase: Developers familiar with the intricacies of a legacy codebase may resist changes to testing practices or frameworks, fearing disruptions to their workflow or uncertainty about the impact on existing functionality. Overcoming this resistance requires effective communication, education about the benefits of addressing flaky tests, and collaboration to devise solutions that mitigate risks.

Time constraints and competing priorities: Software development teams often face time constraints and competing priorities, making it challenging to allocate sufficient resources to address flaky tests in legacy codebases. In a fast-paced environment where deadlines loom, new features take precedence, investing time and effort into test maintenance and improvement may be deprioritized, perpetuating the cycle of flakiness.

SolutionsIn this section, we’ll discuss solutions for overcoming the process challenges encountered in legacy codebases with flaky tests.

Establishing clear ownership and responsibility for test quality within the team: Assigning clear ownership and accountability for test quality within the development team ensures that flaky tests are actively monitored, managed, and resolved. By designating individuals or teams responsible for maintaining test suites and addressing flakiness, organizations can ensure that testing efforts remain consistent and proactive.

Promoting a culture of test automation and continuous improvement: Fostering a culture of test automation and continuous improvement encourages developers to prioritize testing practices and invest in automation tools and frameworks. By emphasizing the importance of test reliability and encouraging collaboration and knowledge sharing among team members, organizations can cultivate an environment where flaky tests are identified and addressed promptly.

Prioritizing flaky test fixes based on impact and feasibility: Prioritizing flaky test fixes based on their impact on software quality and feasibility of resolution allows development teams to allocate resources effectively and focus on addressing the most critical issues first. By conducting impact assessments and feasibility analyses, teams can identify high-priority flaky tests that require immediate attention and develop targeted strategies for resolution, minimizing disruption to development workflows.

Case Studies: Taming Flaky Legacy Tests in ActionThe following case studies illustrate how real-world teams successfully addressed flakiness in their legacy codebases. These examples showcase different approaches that can be adapted to various testing scenarios and legacy system challenges. Let’s delve into these specific examples to see how strategic planning and proactive measures can conquer flakiness in your legacy codebase.

Case Study 1: E-commerce Platform Streamlines Testing An e-commerce platform faced a growing problem with flaky tests in its legacy codebase. These tests caused frequent build failures, hindering development velocity. The team implemented a two-pronged approach:

  • Test Refactoring with Ownership: They established clear test ownership. Each developer became responsible for maintaining the unit tests associated with their code modules. This fostered accountability and encouraged developers to write robust, maintainable tests.
  • Mocking External Dependencies: Legacy tests often relied on external dependencies like databases or external services, causing flakiness. The team implemented mocking frameworks to isolate tests from these dependencies, ensuring consistent testing environments.

The outcome? Build failures caused by flaky tests dropped by 70%. This not only accelerated development but also improved developer confidence in the test suite.

Case Study 2: Financial Services Company Enhances Test Automation

A financial services company struggled with a large suite of manually executed regression tests for its legacy core banking system. These tests were time-consuming, prone to human error, and unreliable. The team embarked on a test automation journey:

  • Prioritization and Automation: The team prioritized critical user journeys and functionalities, focusing automation efforts on these areas first. They utilized automation frameworks to convert manual tests into automated scripts.
  • Flaky Test Detection and Flake Analysis: Tools were implemented to automatically detect flaky tests. Analysis of these tests revealed issues like timing dependencies and external resource contention.

By automating critical test cases and identifying flaky tests, the company significantly reduced regression testing time and improved test suite reliability. Additionally, the insights from flake analysis helped developers fix underlying code issues, leading to a more robust system overall.

ConclusionI remember the frustration of dealing with flaky tests in a legacy codebase at my previous job. Our e-commerce platform, built years ago, had a growing suite of UI tests that were becoming increasingly unreliable. Every build felt like a coin toss – would the tests pass, or would a random failure bring the whole process to a halt? It was a major bottleneck for development.

We tackled the problem head-on, implementing strategies like the Page Object Model for UI tests and mocking external services. Slowly but surely, the flakiness subsided. Tests became dependable, builds went smoothly, and our confidence in the codebase grew significantly. This experience, along with countless others in the industry, underscores the importance of addressing flaky tests in legacy systems.

The post Addressing Flaky Tests in Legacy Codebases: Challenges and Solutions appeared first on Semaphore.

View Details

In this article, we explore React Portals, how they work, and how you can use them effectively in your React applications.

The post A Guide to React Portals appeared first on Semaphore.

View Details

This article explores app marketing strategies for developers to enhance user engagement and differentiate from competitors.

The post How to Successfully Market Your App as a Developer in a Crowded Digital World appeared first on Semaphore.

View Details

This article guides deploying an ERC20 token on Ethereum using Hardhat, emphasizing streamlined development and enhanced security.

The post Deploying ERC20 Token With Hardhat: A Step-by-Step Guide appeared first on Semaphore.

View Details

The article focuses on the ERC20 standard (Request for Comment), which is widely used to create digital tokens on the Ethereum network. It outlines the deployment procedure using Remix EVM (Ethereum Virtual Machine) and the integration of ERC20 tokens with the Solidity programming language.

Developers can apply the knowledge in this article to create their own digital tokens for various purposes, including fundraising, incentive programs, and in-app payments. They can also create decentralized applications (dApps) that utilize these tokens for value and payment exchanges.

While other programming languages are available, Solidity is the popular choice among developers in the decentralized finance (DeFi) space due to its simplicity and ease of use. Understanding the ERC20 standard in Solidity offers several advantages, such as streamlined development processes, a robust community for support and collaboration, and the ability to create innovative solutions for decentralized applications (dApps) and digital token use cases.

A Brief History of the ERC20 TokenOne of the most widely used token specifications on the Ethereum network is ERC20. Due to its simplicity and adaptability, it was quickly adopted after being introduced by Fabian Vogelsteller in November 2015. ERC20 tokens are fungible (mutually interchangeable) and easy to maintain, thanks to smart contracts that establish a set of rules for the tokens to follow.

The standardization of ERC20 tokens has completely transformed the creation and management of tokens by businesses and individuals. Before ERC20, developers found it challenging to ensure compatibility with other tokens and wallets because each token had to be designed from scratch. Now, developers can use ERC20 to create tokens that are compatible with any wallet or exchange that accepts the standard.

Since its inception, thousands of tokens on the Ethereum network—including some of the largest and most valuable, such as Chainlink, Binance Coin, and Tether—have been built on the ERC20 standard. The standard has also influenced several other token standards, including ERC721 (for non-fungible tokens) and ERC1155 (a single smart contract containing multiple tokens). ERC20 has been instrumental in the growth and improvement of the Ethereum ecosystem.

What Is an ERC20 Token?An ERC20 token is a digital asset built on the Ethereum blockchain under the ERC20 standard. These standardized tokens are fungible, meaning that each token can be exchanged for another of equal value. Introduced in 2015 to address issues with smart contract tokens, the ERC20 standard has gained popularity within the Ethereum ecosystem.

On the Ethereum network, ERC20 tokens offer compatibility with a variety of wallets, exchanges, and decentralized applications. They also provide enhanced security and reliability, as well as the ability to be programmed to perform specific tasks. These tokens can represent a range of assets, including digital currencies, company stock, tangible goods, and reputation points, among others.

At the time of writing this article, Ethplorer has documented the total value of all tokens in its database—known as the “Tokens Cap”—at $286.50 billion.

Ethplorer provides information about tokens that operate on the Ethereum blockchain. The tool offers details on various aspects of these tokens, such as market value, pricing, and transaction history.

Benefits of Utilizing ERC20 TokensIn the Ethereum blockchain, token creation is governed by the ERC20 technical standard. Below are some advantages of using ERC20 tokens:

  • Reduced Costs: Because ERC20 tokens eliminate the need for middlemen and enable quicker settlement times compared to traditional financial systems, they have lower transaction fees and costs.
  • Decentralization: ERC20 tokens are decentralized since they are created on the Ethereum blockchain, meaning they are not owned or controlled by a single entity. Because all transactions are recorded and published, the movement and ownership of these tokens are more transparent than with traditional, paper contracts.
  • Programmability: ERC20 tokens are programmable, allowing for the customization of actions to carry out specific tasks, such as enabling automatic payments, voting processes, or access control.
  • Security: Transactions for ERC20 tokens are stored on the immutable Ethereum blockchain, making them highly secure and resistant to hacking, fraud, and other nefarious activities.

How Does an ERC20 Token Work?Tokens must adhere to guidelines outlined by ERC20 to be considered ERC20-compliant. These guidelines require the support of specific computer language features to facilitate easy interoperability across various wallets, exchanges, and decentralized apps. As a result, the Ethereum ecosystem simplifies the trading and use of tokens that comply with the ERC20 standard.

Details of the agreement between the buyer and seller are immediately coded and executed as smart contracts, which are self-executing contracts.

These smart contracts hold the token’s supply and enforce the restrictions specified by the ERC20 standard.

The diagram below illustrates how an ERC20 token functions. Initially, the token owner approves 50 tokens for a spender. The spender can then withdraw up to 50 tokens at any time, representing their allowance. Once the tokens are withdrawn, the spender decides how to utilize them. To spend the tokens, the spender can use the transferFrom function, specifying the parameters of the sender, recipient, and token amount.

An ERC20 token transaction is recorded on the Ethereum blockchain, and the token balance is updated when a user sends an ERC20 token to another user.

Defining the ERC20 Token Standard and Its GuidelinesThe Ethereum blockchain employs a set of rules and specifications known as the ERC20 token standard to produce tokens. ERC stands for “Ethereum Request for Comment,” and the proposal identification assigned to this standard is 20. To create an ERC20 token, you must adhere to the following rules:

  • Token Name: The official name of the token.
  • Symbol: The token’s identifying abbreviation.
  • Decimal Points: The number of decimal places used to represent the token. For example, if the token has 18 decimal places, one token is equal to 1000000000000000000 (10^18) units.
  • Total Supply: The overall quantity of tokens that will be issued.
  • BalanceOf: This function returns the balance of the token.
  • Transfer: This function allows the movement of tokens from one address to another.
  • TransferFrom: This function enables authorized addresses to send tokens from other addresses (as seen in the example above).
  • Approval: This function allows one address to authorize another address to spend tokens on its behalf.
  • Allowance: This function indicates the number of tokens that have been authorized for one address to use on behalf of another (as illustrated above).

The following diagram presents a visual representation of the functioning of the ERC20 token standard.

Following these guidelines ensures that the token will integrate easily with other systems and wallets that accept ERC20 tokens. It also guarantees that the token can be traded on cryptocurrency exchanges that support ERC20 tokens.

Examples of ERC20 Tokens in ActionBelow are just a few examples of ERC20 tokens in action. ERC20 tokens are widely used in the blockchain ecosystem and provide a standard way for creating and managing tokens on the Ethereum blockchain. There are numerous instances of ERC20 tokens being used in the blockchain ecosystem. Here are a few examples:

  • USDT (Tether): Tether is a stable coin that is tied to the US dollar, with each USDT token standing in for one dollar. Tether is an ERC20 token that is frequently used as a stablecoin for trading and money transfers on different cryptocurrency exchanges.
  • LINK (Chainlink): Chainlink is a decentralized oracle network that links smart contracts to outside data sources. The LINK currency is used to reward node operators for providing correct data to the network.
  • BAT (Basic Attention Token): The Brave browser ecosystem uses Basic Attention Token to compensate users for viewing adverts. On the platform, the token can be used to pay for premium products or services.
  • OMG (OmiseGO): OmiseGO is a blockchain-based payment system that enables quick, safe, and inexpensive payments. On the platform, OMG tokens are utilized as a medium of trade.
  • REP (Augur): Augur is a decentralized platform for prediction markets that enables users to build and trade their own. The REP token is used on the platform to report the results of events and to reward truthful reporting.

While there are numerous other widely used tokens in circulation, it is currently unfeasible to present a complete list of them. Nonetheless, a picture displaying some of the well-known tokens can be found below:

The images above provide evidence that many tokens are featured on the CoinCodex website, and it’s apparent that ERC20 tokens are popular. In the next section of this tutorial, we will explore the reasons behind the popularity of ERC20 tokens.

Why Are ERC20 Tokens Popular?ERC20 tokens are popular because of their portability and standardization, which make them easy to use and trade within the Ethereum ecosystem. To maintain compatibility with other tokens and wallets, the standard outlines a set of guidelines that all ERC20-compliant tokens must follow. The ease with which developers can create new tokens and users can interact with them has led to the widespread use of ERC20 tokens across various industries.

Advantages of Using ERC20 Tokens Over Other Types of TokensAnyone interested in the blockchain ecosystem should understand the benefits of using ERC20 tokens over other types of tokens. These tokens can serve multiple purposes, such as trading, user incentives, and fundraising. Compared to other token types, ERC20 tokens offer several advantages:

  • Standardization: To ensure interoperability among various ERC20 tokens, the standard provides a set of rules and guidelines. This standardization makes it easy for developers to create and integrate new ERC20 tokens into existing platforms.
  • Security: ERC20 tokens are based on the Ethereum blockchain, a decentralized and immutable ledger, enhancing their security. This makes it difficult for hackers or other malicious actors to compromise the token’s integrity.
  • Reduced Costs: ERC20 tokens have lower transaction fees and costs because they don’t rely on intermediaries and offer quicker settlement times compared to traditional financial systems.
  • Liquidity: ERC20 tokens are highly liquid, making it easy to buy or sell them on exchanges, thus offering investors greater flexibility in managing their portfolios.

Use Cases for ERC20 TokensUnderstanding the use cases for ERC20 tokens is vital for grasping the potential of blockchain technology and its real-world applications. Here are some common uses:

  • Fundraising: ERC20 tokens can be used for fundraising through Initial Coin Offerings (ICOs) or Security Token Offerings (STOs). Companies and startups can issue tokens based on the ERC20 standard to attract investors.
  • Payment Systems: In various ecosystems, ERC20 tokens can serve as a form of payment. Businesses can create their own ERC20 tokens and use them to pay for goods and services within their ecosystem.
  • Reward Systems: ERC20 tokens can be transformed into points for loyalty programs, offering users rewards for using specific services or platforms.
  • Decentralized Applications: ERC20 tokens can incentivize users to contribute to decentralized applications (dApps). For instance, a dApp offering file storage services can reward users with ERC20 tokens for donating idle disk space.

Notable Examples of ERC20 Tokens and Their Success StoriesSuccessful ERC20 tokens include several prominent examples:

  • USDT (Tether): A stablecoin valued in line with the U.S. dollar, Tether has a market cap of over $50 billion and serves as both a store of value and a medium for transferring wealth between exchanges.
  • UNI (Uniswap): This is an Automated Market Maker (AMM)-based Decentralized Exchange (DEX) that facilitates trading between ERC20 tokens. The UNI token rewards liquidity providers and serves as a governance token for voting on protocol improvements.
  • BNB (Binance Coin): Developed by the Binance cryptocurrency exchange, BNB is accepted as payment for transaction fees and offers users discounts on trading costs.

How to Create an ERC20 TokenThis section will show you how to use OpenZeppelin to create an ERC20 token, as well as generate a Solidity token called “MyToken.” Note that if the name has already been used elsewhere, it won’t impact the token’s uniqueness; the contract address serves as the unique identifier. The ERC20 token symbol will be a short name, “MYT,” which stands for MyToken.

Short names are often generated for ERC20 tokens to enhance convenience, readability, and smart contract efficiency, as well as to facilitate trading, strengthen branding, and enable effective communication across different platforms.

To create your token, we’ll use Remix. Start by creating a folder where you’d like to store your file. Within the folder, create a file named “MyToken.sol.” The “.sol” extension indicates a Solidity file. Once the file is created, copy the code below the GIF output and paste it into the “MyToken.sol” file. The following GIF illustrates the process of creating a Solidity file within a folder and pasting the copied code into the file using Remix.

Below is the Solidity code that will be utilized for creating the Token:

// SPDX-License-Identifier: MITpragma solidity ^0.8.17;import "@openzeppelin/contracts/token/ERC20/ERC20.sol";import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Capped.sol";import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";contract MyToken is ERC20Capped, ERC20Burnable { address payable public owner; uint256 public blockReward; constructor( uint256 cap, uint256 reward ) ERC20("MyToken", "MYT") ERC20Capped(cap * (10 ** decimals())) { owner = payable(msg.sender); _mint(owner, 50000000 * (10 ** decimals())); blockReward = reward * (10 ** decimals()); // Setting block reward for first deploy } // Setting miner reward function _mintMinerReward() internal { _mint(block.coinbase, blockReward); } // block.conbase validation for rewarding the minder; prevents miner from manipulating teh token function _beforeTokenTransfer( address from, address to, uint256 value ) internal virtual override { if ( from != block.coinbase && to != block.coinbase && block.coinbase != address(0) ) { _mintMinerReward(); } super._beforeTokenTransfer(from, to, value); } function _mint( address account, uint256 amount ) internal virtual override(ERC20Capped, ERC20) { require( ERC20.totalSupply() + amount <= cap(), "ERC20Capped: cap exceeded" ); super._mint(account, amount); } //Destroying the contract function destroyContract() public onlyOwner { selfdestruct(owner); } // Set block rewards function setBlockReward(uint256 reward) public onlyOwner { blockReward = reward * (10 ** decimals()); } //reusable modifier modifier onlyOwner() { require(msg.sender == owner, "Only the owner can call this function"); _; }} The Code StructureThe code above is a Solidity smart contract that establishes a custom ERC20 token named “MyToken.” It inherits from three OpenZeppelin ERC20-related contracts: ERC20Capped, ERC20Burnable, and ERC20. These contracts define a maximum token supply, allow tokens to be burned, and provide basic functionality for an ERC20 token.

The contract includes two public state variables: owner and blockReward. The owner variable identifies the owner of the contract, while the blockReward variable specifies the number of tokens awarded to the miner who successfully mines the block containing a transfer. The contract’s constructor function takes two arguments: cap and reward. cap represents the maximum token supply, and reward represents the amount given to miners who successfully mine a block containing a transfer. The constructor sets the owner variable to msg.sender, mints 50,000,000 tokens for the owner, and sets the blockReward variable to reward multiplied by 10181018, the token’s decimal precision. The decimals() function, as defined in the OpenZeppelin ERC20 standard, always returns a value of 18. This means the token can be subdivided into as many as 10181018 individual units, allowing for precise fractional ownership and facilitating small transactions.

Additionally, the contract includes two internal functions: _mintMinerReward() and _mint(). _mintMinerReward() is triggered when a transfer is executed and mints the blockReward amount of tokens to the miner who mined the block containing the transfer. _mint() overrides the _mint() function from ERC20Capped and ERC20 to ensure the total token supply doesn’t exceed the cap. The contract also features a public function, destroyContract(), that allows the owner to terminate the contract and recover any remaining funds. It includes another public function, setBlockReward(), that allows the owner to modify the blockReward value. Finally, the contract uses a modifier, onlyOwner, to restrict access to certain functions to the contract’s owner alone.

Deploying the ContractTo deploy the smart contract, we’ll continue to use Remix, the platform where the contract was developed. Remix offers a user-friendly deployment process that is both simple and straightforward. In this section, we’ll provide a step-by-step guide to deploying the smart contract using Remix. To deploy the contract, follow these steps:

  • Step 1: Click on the “Compile” button, located on the left-hand side of the smart contract interface. The image below highlights the “Auto-Compile” and “Compile” buttons within a red box. If the “Auto-Compile” feature is enabled, there’s no need to manually click on the “Compile” button.

  • Step 2: It is important to ensure that the contract to be deployed is the “MyToken” contract, which is the most important contract on the list, although there might be other sub-contracts listed as well. This is because “MyToken” contract is the one that inherits from another contract. Next, click on the second checkbox to reveal two fields where the cap and the miner’s reward for the tokens can be added. Step 3 shows the result when the dropdown is clicked.

  • Step 3: Once the dropdown mentioned in Step 2 is clicked, the deployment user interface becomes standardized and easy to understand. It is now possible to enter the cap and reward values for the smart contract. For our smart contract, we will set the cap at 100 million and the miner’s reward at 50. After inputting these values, click on the “transact” button to deploy.

  • Step 4: involves the creation of a new contract after deployment. The image below shows the newly created contract, which is indicated by the red box. Please click on it.

  • Step 5: Once you click on the newly created contract, you will be able to see the list of public functions and events that were previously discussed. You are able to view details such as the token name, token symbol, total supply, block reward, and cap values that were added during deployment. It’s important to note that the cap value is initially set at 100 million with 18 zeros. This is a representation of wei, which is the smallest unit.

Potential Dangers and Difficulties with ERC20 TokensDue to security vulnerabilities, ERC20 tokens pose the risk of significant monetary or data loss. Once deployed, contract code cannot be modified, making it challenging to fix security issues. The difficulty in tracing stolen assets from smart contracts further complicates the security of ERC20 tokens. To mitigate these risks, developers must implement robust access control mechanisms and secure smart contracts.

Another potential concern with ERC20 tokens is the threat from backdoors. Attackers can exploit certain ERC20 functions to steal funds or violate users’ privacy, affecting individual accounts or the entire system without consent. Researchers have proposed detection techniques, such as Pied-Piper, which combines datalog analysis and directed fuzzing to identify backdoor risks in ERC20 contracts. Despite their advantages, such as interoperability and programmability, both developers and users must be cognizant of and guard against these potential risks and challenges.

  • https://blog.openzeppelin.com/
  • https://github.com/ConsenSys/smart-contract-best-practices
  • https://blog.openzeppelin.com/

Common Mistakes to Avoid When Developing ERC20 TokensDevelopers should be aware of several common pitfalls when creating an ERC20 token:

  1. Noncompliance with the ERC20 Standard: To ensure compatibility with other tokens and wallets, it’s essential to adhere to the ERC20 standard’s rules and specifications. Noncompliance may result in incompatibility with wallets and other ERC20 tokens.
  2. Insufficient Security Measures: ERC20 tokens are susceptible to various security threats, including hacking and phishing. Developers must implement effective security measures such as multi-factor authentication and encryption.
  3. Lack of Adequate Testing: Insufficient testing can lead to bugs and errors, potentially causing significant issues like financial loss or security breaches. Developers should conduct extensive testing to ensure the token functions as intended and is secure.
  4. Poor Transparency and Documentation: Failing to clearly explain the token’s functionalities can result in misunderstandings and mistakes. Proper documentation is essential for both developers and users to understand how to interact with the token.

ConclusionI hope you found this article informative. It covers all the essentials you need to know before using an ERC20 token. We highlighted the benefits and applications of the ERC20 token and discussed potential risks. While there is much more to learn about ERC20 tokens, I believe this article provides a solid foundation. Feel free to modify the ERC20 token code discussed in this article to build a more complex token or create one for your client. Continue exploring the world of ERC20 tokens and how to generate new ones!

The post Mastering ERC20 Token in Solidity appeared first on Semaphore.

View Details

Redis is an open-source, in-memory datastore. You can use Redis as a messaging system by employing one of its three features: Streams, Pub/Sub, and Lists. Additionally, Redis offers Transaction and Pipeline capabilities to help build performant applications at scale. Lua scripting, server-side functions, and modules further enable you to extend Redis’s functionality.

In this article, we will discuss the Redis data structures that can be employed to build messaging solutions, each illustrated with a sample application written in Go.

What Is Asynchronous Messaging, and How Does It Help with Application Scalability?Asynchronous messaging is a communication pattern in which applications exchange messages without requiring immediate responses. This model involves the sender and receiver decoupling their interactions, which allows them to operate independently and asynchronously. In this paradigm, the sender publishes messages to a messaging system or message broker, which then delivers the messages to one or more recipients. This approach offers several advantages for application scalability.

First, asynchronous messaging enables loose coupling between components, as they don’t need to be aware of each other’s existence. This flexibility facilitates the independent scaling of different system components, which can process messages at their own pace. Second, it helps to mitigate performance bottlenecks by offloading time-consuming tasks to background processes, thereby preventing the main application from becoming unresponsive. By distributing workloads across multiple asynchronous message handlers, applications can accommodate a higher volume of requests and achieve better scalability. Moreover, asynchronous messaging supports the decoupling of different system components, enhancing fault tolerance and resilience; failures or slowdowns in one component do not directly impact others.

Overall, asynchronous messaging promotes scalability through parallel processing, load balancing, fault tolerance, and the independent scaling of system components.

An Overview of Messaging Data Structures in RedisThis section offers a high-level overview of Redis Lists, Pub/Sub, and Streams, along with some essential commands associated with each.

Redis ListA Redis List is a versatile data structure that enables the storage and manipulation of ordered collections of elements. Lists in Redis are implemented as linked lists, providing efficient insertion and deletion operations at both ends of the list.

Redis furnishes a rich set of commands for list operations, including the following:

  • LPUSH: Inserts one or more elements at the beginning of a list. This command can create new lists or add elements to existing ones.
  • RPUSH: Appends one or more elements at the end of a list. Like LPUSH, it can be used for new or existing lists.
  • LPOP: Removes and returns the element at the beginning of a list, useful for processing elements in a queue-like fashion.
  • RPOP: Removes and returns the element at the end of a list, commonly used to implement stack-like behavior.
  • LLEN: Returns the length of a list, indicating the total number of elements.
  • LRANGE: Retrieves a range of elements from a list based on their indices.
  • LINDEX: Returns the element at a specific index in the list.
  • LINSERT: Inserts an element before or after a specified element in the list.

Redis Pub/SubRedis Pub/Sub (Publish/Subscribe) is a messaging pattern that facilitates the broadcasting of messages to multiple subscribers. In this model, a Redis client can act as both a publisher, sending messages to channels, and a subscriber, receiving messages from those channels. Channels are string-based identifiers that serve as communication conduits.

When a publisher disseminates a message to a channel, Redis delivers it to all connected subscribers of that channel. Subscribers can follow multiple channels, enabling concurrent reception of messages from various sources. Although Redis Pub/Sub provides a high-performance message bus, it is ephemeral, meaning messages are not stored for offline consumption.

The following are key commands associated with Redis Pub/Sub:

  • PUBLISH: Sends messages to specific channels.
  • SUBSCRIBE: Allows clients to follow one or more channels.
  • UNSUBSCRIBE: Enables clients to stop following specific channels.
  • PSUBSCRIBE: Permits clients to subscribe to channels using pattern matching.
  • PUNSUBSCRIBE: Unsubscribes clients from channels followed through PSUBSCRIBE.
  • PUBSUB: Provides information about the Pub/Sub system in Redis.

Redis StreamsRedis Streams is a robust data structure designed explicitly for managing high-volume, real-time data streams. Streams offer a way to model and process continuous data flows in a fault-tolerant and scalable manner. Each entry in a stream has a unique ID and consists of a key-value pair, allowing for the inclusion of additional metadata with the message payload.

Here are some essential commands related to Redis Streams:

  • XADD: Appends a new entry to a stream.
  • XLEN: Returns the total number of entries in a stream.
  • XREAD: Enables consumers to read entries from one or more streams.
  • XGROUP: Manages consumer groups within a stream.
  • XREADGROUP: Similar to XREAD, but designed for consumer groups.
  • XACK: Acknowledges the successful processing of an entry by a consumer.
  • XDEL: Deletes one or more entries from a stream based on their IDs.

By understanding and utilizing these Redis data structures and commands, developers can effectively implement scalable messaging solutions.

Choosing the Right Messaging Option in RedisWith a plethora of choices available, selecting the appropriate tool for your specific needs is crucial. When deliberating between Redis List, Pub/Sub, and Redis Streams, it’s important to take into account the particular requirements of your use case. For instance, if your application necessitates a load-balanced queue, Redis List is a sensible choice, as Redis Streams could introduce unwarranted complexity. On the other hand, if your project demands stream processing semantics—coupled with persistence and flexible traversal—Redis Streams is the optimal choice. Last but not least, if the ephemeral nature of Redis Pub/Sub aligns well with your needs and you’re aiming for maximum performance, Redis Pub/Sub is the way to go.

Below is a table that summarizes the advantages and disadvantages of these solutions:

| Data Structure | Benefits | Drawbacks | | --- | --- | --- | | List | Easy to use, present in existing open-source solutions, also provides a reliable API version (BRPOLPUSH). | Does not support stream processing semantics. | | Pub/Sub | Can handle high message volumes | Messages are ephemeral i.e. consumers need to be subscribed in order to receive messages. | | Redis Streams | First-class stream processing capabilities, flexible stream traversal along with advanced features such as consumer groups, auto-claiming and observability. | Complex data structure. Use lists or Pub/Sub if advanced functionality is not required. |

Deep-dive into Redis messagingNow that we have a basic overview of messaging related data structures available in Redis, let’s learn how to use these using Go. For this you will need to install a recent version of Go and have a Redis instance running locally. For example, you can use the following command to start a Redis instance in a Docker container:

docker run --rm -it --name redis -p 6379:6379 redis You can use redis-cli to interact with Redis from the command line. It is automatically installed when you setup Redis for your operating system. However, if you’re using Redis from Docker, you can access redis-cli directly from the Docker container (as per instructions in the subsequent sections).

Create a new Go module and add the required dependencies:

go mod init go-redis-messaging-data-structures

go get github.com/redis/go-redis/v9 Redis ListRedis Lists support blocking operations like BLPOP and BRPOP, which allow applications to wait for a specific duration if the list is empty. Once an item is added to the list—typically via the LPUSH method by another application—it can be immediately fetched and processed. This decoupling enables the creation of asynchronous and event-driven architectures, often referred to as the worker queue pattern, where the list serves as a queue.

Producers populate the queue with items, and worker applications (or consumers) dequeue and process them. By utilizing Redis Lists and their blocking operations, developers can craft efficient and scalable systems for task distribution, message queuing, and background job processing.

Frameworks like Celery and Sidekiq support Redis as a backend for background tasks.

Here’s an example demonstrating the use of a list as a job-processing queue (for processing user data, for example):

package mainimport ("context""errors""fmt""log""time""github.com/redis/go-redis/v9")const listName = "demo-list"func main() {fmt.Println("list consumer application started")client := redis.NewClient(&redis.Options{Addr: "localhost:6379"})_, err := client.Ping(context.Background()).Result()if err != nil {log.Fatal("failed to connect", err)}for {data, err := client.BRPop(context.Background(), 2*time.Second, listName).Result()if err != nil {if errors.Is(err, redis.Nil) {continue}log.Println("brpop operation failed", err)}fmt.Println("received data from list -", data[1])}} Before we run the program, let’s get an understanding of what’s going on.

  • We start by connecting to Redis. In this case it’s a local instance on localhost and port 6379.
  • We Ping Redis to check the connection to the Redis server. If there is an error, the program exits.
  • A loop is started to continuously consume data from the Redis List.
    • Within the loop, the BRPop function is used to block and wait for the next available element to be popped from the Redis List. It takes a context, a timeout duration (2 seconds in this case), and the name of the list. If there are no elements available within the timeout, the command returns a redis.Nil error. If there is any other error, it is logged, but the program continues to the next iteration.
    • If there is data returned by the BRPop function, it is printed to the console – BRPop returns a slice where the first element is the name of the list and the second element is the popped data.
  • Finally, the program goes back to the beginning of the loop to consume the next element from the Redis List.

To run the above program, copy it to a file named main.go and execute the following command:

go run main.go Produce data using redis-cli.

docker exec -it redis redis-cli lpush demo-list user1docker exec -it redis redis-cli lpush demo-list user2 You should see the following output:

list consumer application startedreceived data from list - user1received data from list - user2 Start another instance of the consumer application in a new terminal window.

go run main.go Continue to produce data using redis-cli.

docker exec -it redis redis-cli lpush demo-list user3docker exec -it redis redis-cli lpush demo-list user4docker exec -it redis redis-cli lpush demo-list user5docker exec -it redis redis-cli lpush demo-list user5 These messages will be consumed by the two consumer instances in a load-balanced way. For example, the second consumer application instance might receive user3 and user5, while the first instance might receive user4 and user5.

You can also start additional consumer instances to see how the messages are distributed among them.

Redis Pub/SubRedis Pub/Sub offers a high-performance messaging system. When integrated with other technologies such as WebSocket, it can be employed to construct real-time applications, including chat platforms, stock tickers, and more.

WebSocket is a communication protocol outlined in RFC 6455 that facilitates bidirectional, full-duplex communication between a client and a server over a single TCP connection. Full-duplex communication means that both the client and server can send messages to each other at any time, enabling simultaneous message exchange. This offers an improvement over other communication patterns, such as long polling, which are commonly used in HTTP-based solutions, particularly for real-time applications. By establishing a persistent connection, WebSocket eliminates the need for repeated HTTP requests, thereby allowing for efficient and instantaneous communication between the client and server.

In a chat-like application, WebSocket and Redis Pub/Sub can be combined to offer a highly scalable architecture that accommodates numerous concurrent users. Redis Pub/Sub can address some of WebSocket’s limitations, including stateful connections, which can impede the system’s scalability by restricting it to a single application instance that users can connect to via a WebSocket session. When scaling out horizontally with multiple application instances, Redis Pub/Sub aids by broadcasting messages to all connected users, irrespective of the application instance to which they are connected.

Example: Building a Chat Application with WebSocket and Pub/Sub

package mainimport ("context""fmt""log""net/http""os""os/signal""strings""syscall""time""github.com/gorilla/websocket""github.com/redis/go-redis/v9")var client *redis.Clientvar Users map[string]*websocket.Connvar sub *redis.PubSubconst channelName = "demo-chat"func init() {Users = map[string]*websocket.Conn{}}func main() {client = redis.NewClient(&redis.Options{Addr: "localhost:6379"})_, err := client.Ping(context.Background()).Result()if err != nil {log.Fatal("ping failed. could not connect", err)}go func() {sub = client.Subscribe(context.Background(), channelName)messages := sub.Channel()for message := range messages {from := strings.Split(message.Payload, ":")[0]for user, peer := range Users {if from != user {msg := "[" + from + " says]: " + string(strings.Split(message.Payload, ":")[1])peer.WriteMessage(websocket.TextMessage, []byte(msg))}}}}()http.HandleFunc("/chat/", chat)server := http.Server{Addr: ":8080", Handler: nil}go func() {fmt.Println("chat server started")err := server.ListenAndServe()if err != nil && err != http.ErrServerClosed {log.Fatal("failed to start server", err)}}()exit := make(chan os.Signal, 1)signal.Notify(exit, syscall.SIGTERM, syscall.SIGINT)<-exitfmt.Println("exit signalled")ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)defer cancel()for _, conn := range Users {conn.Close()}sub.Unsubscribe(context.Background(), channelName)sub.Close()server.Shutdown(ctx)fmt.Println("chat application closed")}var upgrader = websocket.Upgrader{}func chat(w http.ResponseWriter, r *http.Request) {user := strings.TrimPrefix(r.URL.Path, "/chat/")upgrader.CheckOrigin = func(r *http.Request) bool {return true}c, err := upgrader.Upgrade(w, r, nil)if err != nil {log.Fatal("protocol upgrade error", err)return}Users[user] = cfmt.Println(user, "joined the chat")for {_, message, err := c.ReadMessage()if err != nil {_, ok := err.(*websocket.CloseError)if ok {fmt.Println("connection closed by:", user)err := c.Close()if err != nil {fmt.Println("close connection error", err)}delete(Users, user)fmt.Println("connection and user session closed")}break}client.Publish(context.Background(), channelName, user+":"+string(message)).Err()if err != nil {fmt.Println("publish error", err)}}} Before running the chat application, let’s understand its components:

  1. The Users map associates usernames with their respective WebSocket client session objects.
  2. We start by connecting to a local Redis instance on localhost and port 6379, using Ping to verify the connection. If an error occurs, the program exits.
  3. A goroutine is initiated to subscribe to the Redis Pub/Sub channel. This goroutine listens for incoming messages and broadcasts them to all connected users, excluding the sender.
  4. An HTTP endpoint is registered using http.HandleFunc(), mapped to the /chat/ route. This endpoint handles WebSocket connections for users joining the chat.
  5. An HTTP server is set up via http.Server, configured to listen on port 8080 and launched in its dedicated goroutine.
    • An exit channel is created to capture termination signals (SIGTERM and SIGINT).
    • The program remains idle until a termination signal is received on this channel.
  6. Upon receiving the termination signal, the program commences a graceful shutdown: it closes all WebSocket connections, unsubscribes from the Redis Pub/Sub channel, and shuts down the HTTP server within a timeout of 10 seconds.
  7. The chat() function manages WebSocket connections for users joining the chat. It extracts the username from the URL path and upgrades the HTTP connection to a WebSocket connection.
    • This function adds the WebSocket connection to the Users map and starts listening for incoming messages from the user.
    • When a message arrives, it is published to the Redis Pub/Sub channel with the username as a prefix. The message is then broadcast to all connected users, except for the sender.
    • If an error arises during message reading, the function checks for a close error. If found, the function terminates the connection, removes the user from the Users map, and ends the user’s session.

To run the above program, copy it to a file named main.go and execute the following command:

go get github.com/gorilla/websocketgo run main.go You should see the following output: “chat server started”.

To test the application, use any WebSocket client. In this article, I will be using the command line client wscat.

Join the chat as a new user. In a new terminal:

wscat --connect ws://localhost:8080/chat/shaun Once connected, you should see this output:

Connected (press CTRL+C to quit)> Open another terminal and join as a different user:

wscat --connect ws://localhost:8080/chat/murphy Now you can exchange messages between these two users using the respective terminals. Here is an example:

Redis StreamsConsumer groups in Redis Streams enable the distribution of the workload for message consumption across multiple consumers. When several consumers belong to a consumer group, each is allocated a subset of the stream’s messages to process. This design fosters parallel and distributed processing of the stream, enhancing throughput and facilitating scalability. Consumers within a group operate independently and handle messages concurrently, making the architecture well-suited for high-performance and fault-tolerant systems.

To establish consumer groups, initiate the group using the XGROUP CREATE command. This command requires the stream name, group name, and an initial ID (usually “0-0”). This action sets up the consumer group and designates the starting point for message consumption. Subsequently, consumers are added to the group using the XGROUP SETID command. This command needs the consumer name and the ID of the last message the consumer processed, thereby assigning the consumer to the group and determining their designated segment of the stream.

Messages are consumed from a consumer group through the XREADGROUP command, which fetches pending messages for a particular consumer. This command allows you to input the group name, consumer name, and either specific stream names or > to consume from all available streams. After processing a message, consumers acknowledge it using the XACK command, notifying the consumer group that the message has been successfully processed.

Example: Using Consumer Groups in Redis Streams

package mainimport ("context""errors""fmt""log""time""github.com/redis/go-redis/v9")const stream = "demo-stream"const consumerGroupName = "demo-group"func main() {fmt.Println("redis streams consumer application started")client := redis.NewClient(&redis.Options{Addr: "localhost:6379"})ctx := context.Background()_, err := client.Ping(ctx).Result()if err != nil {log.Fatal("failed to connect", err)}client.XGroupCreateMkStream(ctx, stream, consumerGroupName, "$")for {result, err := client.XReadGroup(ctx, &redis.XReadGroupArgs{Streams: []string{stream, ">"},Group: consumerGroupName,Block: 1 * time.Second,}).Result()if err != nil {if errors.Is(err, redis.Nil) {continue}log.Fatal("xreadgroup error", err)}for _, s := range result {for _, message := range s.Messages {fmt.Println("got data from stream -", message.Values)client.XAck(ctx, stream, consumerGroupName, message.ID).Err()if err != nil {log.Fatal("xack failed for message ", message.ID, err)}fmt.Println("acknowledged message", message.ID)}}}} Before executing the program, let’s understand its functionality:

  • We start, as always, by connecting to Redis. In this case, it’s a local instance on localhost and port 6379. The Ping command checks the connection to the Redis server. If an error is detected, the program exits.
  • The XGroupCreateMkStream function creates a new consumer group called “demo-group” on the “demo-stream” Redis stream. The $ symbol is passed as the last ID argument, specifying that the consumer group should begin consuming from the earliest available message.
  • A loop commences to continuously read messages from the consumer group.
    • Within the loop, the XReadGroup function fetches messages from the “demo-stream,” utilizing the “demo-group” consumer group. The Streams parameter is set to []string{stream, ">"}, instructing it to read from “demo-stream” starting with the most recent available message. The Block parameter is set to one second, causing the command to pause for one second if no messages are available.
    • If an error occurs while executing XReadGroup, the program determines whether the error is a redis.Nil error, signifying no available messages. In this scenario, the loop advances to the next iteration.
    • If messages are returned by XReadGroup, the program iterates through the results and messages, printing the values of each retrieved message.
    • After each message is processed, the XAck function acknowledges the message. This informs the consumer group that the message has been processed successfully and can be removed from pending messages. If an error occurs during this acknowledgment, the program exits.
  • Finally, the loop restarts to read the next batch of messages from the Redis Stream.

To run the above program, copy it to a file named main.go and execute the following command:

go run main.go Produce data using redis-cli.

docker exec -it redis redis-cli Once you’re connected, you should see this prompt:

127.0.0.1:6379> Use XADD to send data to the Redis Stream (demo-stream):

xadd demo-stream * name john email jdoe@test.comxadd demo-stream * tom tom@test.com You should see the following output (the message IDs will be different in your case):

redis streams consumer application startedgot data from streamname: john | email: jdoe@test.comacknowledged message 1686365267267-0got data from streamname: tom | email: tom@test.comacknowledged message 1686365268153-0... Start another instance of the Redis Streams consumer application in a new terminal window.

go run main.go Continue to produce data using redis-cli.

xadd demo-stream * michael michael@test.comxadd demo-stream * sarah sarah@test.comxadd demo-stream * shaun shaun@test.com These messages will be consumed by the two consumer group instances in a load-balanced way. For example, the second consumer application instance might receive michael and sarah, while the first instance might receive shaun.

You can also start additional instances to see how the messages are distributed among them.

Redis Streams also offer commands for managing pending messages. The XPENDING command enables you to obtain information about pending messages in a consumer group. This information includes the number of pending messages and the IDs of both the earliest and latest pending messages. The XCLAIM command allows a consumer to assert ownership of a pending message and commence its processing.

ConclusionIn this article, we explored the plethora of options that Redis offers for constructing scalable, messaging-based solutions. We provided overviews of Redis List, Redis Pub/Sub, and Redis Streams, highlighting the key differences and offering guidance on which solution to use under various circumstances. Finally, we showcased practical examples illustrating how to employ these data structures in your applications.

The post Building Scalable Applications Using Redis as a Message Broker appeared first on Semaphore.

View Details

Learn how to skillfully build a cover letter generator app using Next.js and ChatGPT's powerful machine-learning model.

The post Build a Compelling Cover Letter Writing App With NextJS and ChatGPT appeared first on Semaphore.

View Details

As the use of microservices continues to grow, the importance of performance monitoring tools will also rise.

The post Monitoring Performance in Microservices Architecture appeared first on Semaphore.

View Details

This is a tutorial on how to build a dynamic newsletter sign-up feature using Vue and Firebase Cloud Firestore.

The post Creating a Newsletter Page With Vue and Firebase Cloud Firestore appeared first on Semaphore.

View Details

This article discusses essential tools for Kubernetes engineers to streamline workflows and manage containerized applications efficiently.

The post Top 10 Must-Have Tools for Kubernetes Engineers appeared first on Semaphore.

View Details

The article examines SPAs, their challenges, and new integrated solutions like React, Next.js, and Qwik for improved performance.

The post SPA Are Dead, Long Live SPA appeared first on Semaphore.

View Details

The article highlights design systems as strategic tools bridging engineering and design, ensuring uniformity across products.

The post What Is a Front-End Design System, and Why Is It Necessary? appeared first on Semaphore.

View Details

This article examines how businesses can leverage data sharing to make informed decisions.

The post How to Leverage Data Sharing to Make Better Business Decisions at Scale appeared first on Semaphore.

View Details

This article explores AI's impact on software engineers, highlighting benefits and tools that boost their productivity.

The post Will AI Take Software Engineering Jobs? appeared first on Semaphore.

View Details

Being a junior software developer can be challenging, but by following these eight tips, you can set yourself up for success.

The post 8 Tips for Junior Software Developers appeared first on Semaphore.

View Details

Explore the guide to OpenAI's GPT generations (GPT-3 to GPT-4) and learn how to choose the best text model for your AI applications.

The post A Guide to OpenAI: How to Choose the Best Language Model For Your AI Application appeared first on Semaphore.

View Details

This article discusses tools and techniques for measuring, monitoring, and troubleshooting performance issues in React Native apps.

The post React Native Performance Measuring and Monitoring Tools appeared first on Semaphore.

View Details

Mey Beisaron shares her journey from backend developer to DevOps engineer.

The post Mey Beisaron on Moving From Backend Developer to DevOps: A Journey into the Dark Side of Tech appeared first on Semaphore.

View Details

Exploring OpenAI's GPT new function calling feature, allowing chatbots to interact with external tools and APIs. Unlock the potential of AI-powered applications with this feature.

The post Function Calling: Integrate Your GPT Chatbot With Anything appeared first on Semaphore.

View Details

This article explores gRPC benefits in NestJS for microservices, comparing them with REST and SOAP protocols.

The post Microservices Communication in NestJS With gRPC appeared first on Semaphore.

View Details

This article provides an in-depth look at the responsibilities and qualifications required to become a DevOps engineer.

The post A Quick Overview of the DevOps Engineer Role appeared first on Semaphore.

View Details

The article explores BLoC and Redux choices based on project factors, ecosystems, and their role in building Flutter apps.

The post Understanding State Management in Flutter (Part 3) appeared first on Semaphore.

View Details

This article explores Flutter's InheritedWidget and Provider package, key for efficient, reusable code and state management.

The post Understanding State Management in Flutter (Part 2) appeared first on Semaphore.

View Details

Strengthen security, streamline user management, and simplify authentication with Okta integration.

The post Introducing Okta integration appeared first on Semaphore.

View Details

We delve into the world of open source and business with Percona’s founder Peter Zaitsev.

The post Peter Zaitsev on The Evolution and Challenges of The Open Source Space appeared first on Semaphore.

View Details

Learn about the various state management techniques available in Flutter and discover how to select the most appropriate approach for your project.

The post Understanding State Management in Flutter (part 1) appeared first on Semaphore.

View Details

Explore gRPC: a scalable, high-performance framework for efficient distributed systems, ideal for microservices and real-time apps.

The post An Introduction to gRPC: Building Distributed Systems With Efficiency and Scalability in Mind appeared first on Semaphore.

View Details

Learn what a routing layer is, understand some of the several benefits it can bring to your architecture, and how to build it in React.

The post How to Build a Routing Layer in React and Why You Need It appeared first on Semaphore.

View Details

Explore the critical balance of using project dependencies: understand potential dangers, learn essential checks, and optimize your code security with these six crucial tips.

The post Careful With Your Imports: 6 Things to Know About Project Dependencies appeared first on Semaphore.

View Details

Learn how to build a smarter bot using embeddings, GPT-3, and Python. Leverage the power of word vectors to enhance your chatbot's responses.

The post Word Embeddings: Giving Your ChatBot Context For Better Answers appeared first on Semaphore.

View Details

Michael Feathers shares insights on onboarding teams, scaling software development, and integrating AI.

The post Michael Feathers On Facilitating Onboarding and Scaling in Software Development appeared first on Semaphore.

View Details

Learn to build and test PHP microservices. Adding another microservice involves developing and integrating with an API gateway, but to a different route.

The post Building and Testing PHP Microservices With Semaphore appeared first on Semaphore.

View Details

In this blog post, we will explore the importance of emotional intelligence in effective leadership and how it can impact the success of an organization.

The post The Importance of Emotional Intelligence in Effective Leadership appeared first on Semaphore.

View Details

Learn how to create SVG animations using Tailwind CSS. Additionally, discover the use cases of animated SVGs.

The post Creating SVG Animations Using Tailwind CSS appeared first on Semaphore.

View Details

MRSK is a lightweight Kubernetes to run containerized workloads on any hardware with all the benefits the cloud have accustomed us to.

The post Breaking Free From the Cloud With Mrsk: Just Enough Orchestration for Your Apps appeared first on Semaphore.

View Details

This article examines Redis high-availability architectures, discussing pros and cons and offering guidance on the best fit for each use case.

The post Understanding Redis High-Availability Architectures appeared first on Semaphore.

View Details

Jeff Atwood shares his insights on building communities, the role of AI assistants, and his perspectives on the democratization of digital platforms.

The post Jeff Atwood on Empowering Programmers and Digital Communities appeared first on Semaphore.

View Details

In this article, we'll explore how to implement chaos testing using k6 for two backend services, to help you ensure the resilience of your application.

The post How to Implement Chaos Testing for Your Backend Services Using k6 appeared first on Semaphore.

View Details

Discover how to effectively manage stateful applications in Kubernetes.

The post Managing Stateful Applications on Kubernetes: Challenges and Best Practices appeared first on Semaphore.

View Details

2023 has brought a plethora of game-changing databases to the table. Let's explore some innovative and futuristic databases and compare their features. 

The post Exploring 8 Futuristic Databases to Watch in 2023 appeared first on Semaphore.

View Details

This article will walk you through an in-depth understanding of DAST tools and how to choose the right one for your security operations.

The post Identifying Vulnerabilities in Running Applications Using DAST appeared first on Semaphore.

View Details

In this article, we provide a comprehensive overview of service mesh technology, including the current state of service mesh providers in 2023, use cases for service mesh technology, and an exploration of its future potential.

The post The Service Mesh Landscape in 2023 appeared first on Semaphore.

View Details

Kevlin Henney provides valuable insights into the significance of refactoring, managing technical debt, and achieving a balanced workload.

The post Kevlin Henney on Code Refactoring appeared first on Semaphore.

View Details

By following the best practices for managing secrets in Android CI/CD workflows, developers can use Semaphore to automate their build, test, and deployment processes.

The post Managing Secrets and Sensitive Data in Android CI/CD With Semaphore appeared first on Semaphore.

View Details

This article explores the world of low-code and zero-code development tools, the benefits, key features, use cases, challenges, and the future of this emerging field. 

The post Low-Code and No-Code Development Tools appeared first on Semaphore.

View Details

Kubernetes Ingress is a critical concept to understand if you truly want to know Kubernetes. In this article, I will explain what Kubernetes Ingress is, why we use Kubernetes Ingress, and its benefits. Finally, we will look at a hands-on approach to getting started with Kubernetes Ingress.

Without further ado, let’s get right into it.

What is Kubernetes Ingress?From the Kubernetes Docs, it says that Ingress is:

An API object that manages external access to the services in a cluster, typically HTTP.

The definition here is spot on. As stated earlier, Ingress is an API object that manages external access to services in a Kubernetes cluster. Using ingress gives us the ability to access our services in our Kubernetes cluster.

It is an alternative to using the Service object in Kubernetes. It is more secure because there are no ports exposed externally for the user to use for accessing our application.

There are two key components to understand when it comes to implementing Ingress in a cluster. They are:

  • Ingress Object
  • Ingress Controller

Ingress Object: an Ingress Object is a resource that defines the rules for routing external traffic to services within the cluster. It defines the routing rules which handle how external requests get routed to internal services.

Request protocols (HTTP and HTTPS) are also defined when creating your ingress object. An Ingress Object is basically the manifest file for creating an Ingress resource in a Kubernetes cluster.

Here is an example of what an Ingress Object looks like:

apiVersion: networking.k8s.io/v1kind: Ingressmetadata: name: ingress-objectspec: rules: - host: mydomain.com http: paths: - path: /app1 pathType: Prefix backend: service: name: app1-service port: name: http - path: /app2 pathType: Prefix backend: service: name: app2-service port: name: http In this example, we gave our ingress object the name ingress-object.

We can also see that under the rules, we have some things defined, such as:

  • host: the host holds a valid domain name to which our requests will be routed.
  • http: http defines the paths, services and ports we are mapping any other request with any specified paths defined in the paths parameter.

Note: this can also be an https protocol if you have SSL configured in your Kubernetes Cluster.

Ingress Controller: An Ingress Controller is a significant component of a Kubernetes Cluster.

It serves as the gateway for incoming requests from outside the cluster. The Ingress Controller can also perform a variety of tasks such as Load Balancing, authentication and authorization, etc.

Some examples of Ingress controllers include Nginx, Traefik, etc.

Why should you use Kubernetes Ingress?Ingress is a secure solution that helps us access any service we have in our Kubernetes cluster. Now, the question is how is Ingress more secure than a Service when exposing your application to the outside world?

Ingress is more secure than a Service because it does not expose any ports when you want to allow external traffic into your cluster. For instance, when you have a simple flask application running in your cluster, and you want to expose the application via a service. One of the many ways to do this is to have a service of the type NodePort and expose a specific port on your cluster node.

Alternatively, if you use an Ingress to expose your application, all you have to do is to set up a routing rule to map any incoming request to the service connected to your application. When using Ingress, we still have to use services, but the difference is that now our services will not have the type NodePort exposing any ports to the outside world. The type of service we will have will be a service with the type ClusterIP. This way, we are only allowing internal network connection between all the services within our cluster, making it more secure.

Benefits of Kubernetes IngressThere are some benefits that come with using Ingress, some of which include:

  • Improved cluster security
  • Better traffic routing
  • Improved Load Balancing
  • Automatic Scaling

Getting started with Kubernetes IngressIn this section, we will look at how to get started with Kubernetes Ingress.

This is going to be a tutorial, and in this tutorial, we will go through the following sections:

  • Setting up an Ingress Controller in Minikube
  • Understanding Routing in Ingress

PrerequisitesTo follow this tutorial, you will have to meet the following requirements:

  • A working knowledge of Kubernetes.
  • Minkube installed.
  • Kubectl installed.

Setting up an Ingress Controller in MinikubeFor this tutorial, we will be using the Nginx Ingress Controller.

Firstly, we need to make sure that we have Minikube up and running.

We can start up Minikube with this command:

minikube start After starting up Minikube, we need to install the Nginx controller. To do that, enter this command:

minikube addons enable ingress We can make sure that the Nginx controller has been installed successfully by listing out the namespaces we have in our cluster with this command:

kubectl get namespace ingress-nginx will show up as one of the namespaces on Minikube.

Understanding routing in IngressNow that we have installed our Nginx ingress controller, let us look at how we can create an ingress object and use it to configure our routing rules.

Firstly, we need to create pods and services with which we are going to define routing rules for our ingress object.

We will create a backend pod and backend service. The backend pod will be a flask server, while the backend service will be a service of the type ClusterIP, pointing to the backend pod.

To create our backend pod, we need to create a backend.yml file and add this piece of code to it.

apiVersion: apps/v1kind: Deploymentmetadata: name: backendspec: selector: matchLabels: app: backend template: metadata: labels: app: backend spec: containers: - name: backend image: callspadeoz123/flask-docker:latest resources: limits: memory: "128Mi" cpu: "200m" ports: - containerPort: 5000 For the backend service, create a file called backend-svc.yml and add this code to it:

apiVersion: v1kind: Servicemetadata: name: backend-svcspec: selector: app: backend type: ClusterIP ports: - port: 5000 targetPort: 5000 Next, we need to deploy both the deployment and the service in our Minikube cluster.

We can do that with this command:

kubectl apply -f backend.ymlkubectl apply -f backend-svc.yml We can check to see if our pod and service are up and running with this command:

kubectl get pod kubectl get service After confirming that both our service and pod are up and running, we can now create our ingress object.

To create our ingress object, create a file called ingress.yml (this file can be called anything you want as long as it’s a yml file) and add this block of code:

apiVersion: networking.k8s.io/v1kind: Ingressmetadata: name: my-ingress labels: name: my-ingressspec: rules: - host: flask-server.com http: paths: - pathType: Prefix path: "/" backend: service: name: backend-svc port: number: 5000 As you can see in the spec scalar, there is another scalar under it called rules.

The rules scalar hold other important scalars such as:

  • host: the host is the domain name that must be a valid domain that the ingress can use to route external requests from.
  • protocol(http/https): the protocol can either http or https. In this case, we are using http.
  • path: the path is the path to the domain name we add to our browser
  • pathType: the pathType denotes the type of path we are adding to the host.
  • backend: the backend defines the service and port the request should be routed to internally.

Next, we need to apply our ingress.yml file like so:

kubectl apply -f ingress.yml If we look closely, we can see that the name of our host is flask-server.com, but this is not a registered domain on the internet. We can make it work on our local machine with a bit of effort.

To register a domain name on our local machine, we need to map the IP address of the server to any name we want.

The IP address of our ingress server can be obtained by running this command:

kubectl get ingress Note: After applying our ingress manifest, it will take some time (about a minute) for it to initialize an IP address for us to use.

We should get an output like this:

NAME CLASS HOSTS ADDRESS PORTS AGEmy-ingress nginx flask-server.com 192.168.59.100 80 102m Note: your IP will be different.

Now, to map the IP address to the hostname, open the hosts file located in your etc directory for editing, like so:

sudo nano /etc/hosts Within the file, add the hostname and IP address like this:

Now that we have configured our host, we can visit the host in our web browser, and we should see our flask app up and running.

We can also have more routes to our ingress object:

apiVersion: networking.k8s.io/v1kind: Ingressmetadata: name: my-ingress labels: name: my-ingressspec: rules: - host: flask-server.com http: paths: - pathType: Prefix path: "/" backend: service: name: backend-svc port: number: 5000 - pathType: Prefix path: "/path" backend: service: name: serviceName port: number: servicePort ConclusionIf you found Kubernetes Ingress a confusing topic, I hope I helped demystify and simplify it for you. It’s a tricky concept to understand at first, but when you get the hang of it, and constantly practice building more with it, it becomes very easy to use.

We also looked at some of the benefits of using Ingress, including: load balancing, routing management, easy scalability, etc.

I hope you found this article helpful and informative. Stick around for more DevOps content!

The post Getting Started With Kubernetes Ingress appeared first on Semaphore.

View Details

Imagine a chef working in a busy restaurant kitchen. In a synchronous kitchen, the chef would prepare one dish at a time, waiting for each dish to finish cooking before starting the next one. This can be slow and inefficient, especially when many orders are in the queue. In an asynchronous kitchen, however, the chef could work on multiple dishes simultaneously, checking on each one periodically and adjusting the cooking times as necessary.

This allows the chef to fulfill orders quickly and ensures that customers receive their food as soon as possible. Similarly, asynchronous programming enables computer programs to perform multiple tasks concurrently, resulting in faster and more efficient execution.

JavaScript is single-threaded, which means that only one line of code can be executed at a time.

This can be beneficial for performance, as it allows for more efficient use of resources, but it can also be a limitation when dealing with complex tasks that require multiple threads of execution.

In this tutorial, we’ll go over the asynchronous components of JavaScript.

What is asynchronous Javascript?Asynchronicity means that if JavaScript has to wait for an operation to complete, it will execute the rest of the code while waiting.

Note that JavaScript is single-threaded. This means that it carries out asynchronous operations via the callback queue and event loop.

In synchronous JavaScript, each function is performed in turn, waiting for the previous one to complete before executing the subsequent one. Synchronous code is written from top to bottom.

The nature of JavaScript and why async programming is requiredJavaScript is single-threaded and has a global execution context, as we learned earlier.

As a result, by design, JavaScript is synchronous and has a single call stack. Code will be executed in a last-in, first-out (LIFO) order in which it is called.

To understand what synchronous JavaScript means, let us consider the code snippet below.

//synchronous javascriptconsole.log("synchronous.");console.log("synchronous javascript!")console.log("synchronous again!") If we go ahead and run the above code, we get:

//Output"synchronous""synchronous javascript!""synchronous again!" This shows that synchronous JavaScript runs from top to bottom, as we can observe the console running the function linearly from top to bottom.

With the code above, the console first logs synchronous to the terminal, then synchronous javascript! and synchronous again!. This is an example of synchronous JavaScript since the program executes code in the order it is written.

Async programming is vital because it allows multiple processes to run concurrently without interfering with the main thread.

This is significant because the main thread is in charge of managing the call stack, which is a data structure that holds the current sequence of function calls.

Blockage of the main thread leads to decreased performance because async programming permits the main thread to stay unblocked, and additional tasks can be completed while the asynchronous task is ongoing.

Let’s look at a JavaScript function call called the setTimeout() function, which allows us to run javascript code after a certain amount of time.

After the specified time, the setTimeout() method executes a block of code. The method only runs the code once.

To work with the setTimeout() function, we need to use an arrow function that is present in ES6, as shown below:

//Asynchronous JavascriptsetTimeout(function, milliseconds); These are the parameters:

  1. function: a function that includes a code block.
  2. milliseconds: the time it takes for the function to be executed

To explain this, let’s look at the code below:

console.log("asynchronous.");setTimeout(() =>console.log("asynchronous javascript!"), 3000);console.log("asynchronous again!"); The code above won’t run synchronously on the JavaScript engine, in contrast to our prior example.

Let’s look at the output displayed below:

// asynchronous.// asynchronous again!// asynchronous javascript! Step 1: The first line of code will be executed, logging the string asynchronous to the console.

Step 2: The setTimeout method is invoked, which will execute the anonymous function after 3 seconds (3,000 milliseconds). The anonymous function will log asynchronous javascript! to the console.

Step 3: The third line of code will be executed, logging the string asynchronous again! to the console.

Step 4: After 3 seconds, the anonymous function from the setTimeout method will be executed, logging asynchronous javascript! to the console.

In other words, with asynchronous JavaScript, the JavaScript doesn’t wait for answers before continuing to execute subsequent functions.

This is useful for applications that require a lot of processing power since it permits numerous tasks to be completed at the same time.

Techniques for writing asynchronous JavaScriptJavaScript can behave in an asynchronous way. Let’s have a look at some techniques that are useful for asynchronous JavaScript:

We will discuss:

  • promises
  • async/await
  • callbacks

Callbacks: this allows for asynchronous code to be written in a synchronous fashion.

Promises: writing asynchronous JavaScript code is made easy with promises. They enable you to create code that runs after a predetermined period of time or when a predetermined condition is satisfied.

Async/await: async/await is a more recent technique for creating asynchronous JavaScript code. It helps you write more succinct and readable code that will execute after a set period of time or when a set condition is met.

CallbacksIn JavaScript, functions are first-class citizens. Therefore, you can pass a function to another function as an argument. By definition, a callback is a function that executes after the outer code call has finished running. It is supplied as an input to another function. When a function has completed its purpose, it is utilized to enable it to invoke another function.

Let’s take a look at the code example below:

function incrementDigits(callback) { callback();} The incrementDigits( ) function takes another function as a parameter and calls it inside. This is valid in JavaScript and we call it a callback. So, a function that is passed to another function as a parameter is a callback function.

In order to carry out asynchronous actions in JavaScript, such as executing an AJAX request or anticipating a user click, callbacks are widely utilized. Having learned about the setTimeout() function in the previous section, You can also write callback functions as an ES6 arrow function, which is a newer type of function in JavaScript:

setTimeout(() => { console.log("output is initiated after 5 seconds");}, 5000); The setTimeout() method is being used in this code to postpone the execution of a function. It will log the string output is begun after 5 seconds to the console using the function, which in this case is an arrow function. The provided delay is 5,000 milliseconds (or 5 seconds). After the asynchronous operation is complete, the callback function is invoked with the operation’s result.

function incrementDigits(num, callback) { setTimeout(function() { num++; console.log(num); if (num < 10) { incrementDigits(num, callback); } else { callback(); } }, 1000);}incrementDigits(0, function() { console.log('done!');}); This code is an example of callback hell. It is a recursive function that calls itself until the number is greater than 10. Then, it executes the callback function. The setTimeout() function is used to delay the execution of the code in order to simulate a longer process. The function will print out each number, starting at 0, until it reaches 10, and then the done! message will be printed out. As a result, the code can run asynchronously, which means that the functions can run concurrently rather than waiting for one another to complete. We have already seen how to create an asynchronous callback in the previous section.

How asynchronous functions and callbacks corks under the hoodWhen an asynchronous function encounters the term awaits, it stops execution until the promise is resolved, while an asynchronous callback is a higher-order function that executes the asynchronous callback function in a non-blocking way. While a promise is fixed, the asynchronous function stops its execution.

We can write asynchronous functions as if they were synchronous (executed sequentially) with the use of await, which stops execution until the promise is resolved (execution successful).

Handling callback hellIn JavaScript, the way to create a callback function is to pass it as a parameter to another function and then call it back after the task is completed.

There are ways to handle callbacks.

  1. Use of promise: a promise can be generated for each callback. When the callback is successful, the promise is resolved, and if the callback fails, the promise is rejected.
  2. Use of async-await: asynchronous functions are executed sequentially with the use of await; execution stops until the promise is revolved and function execution is successful.

PromiseAn async promise operation’s eventual success or failure is represented as a JavaScript object. It enables the creation of asynchronous code that works and appears synchronous. A promise, in our context, is something that will take some time to do. A promise has three possible states: pending, fulfilled, or rejected.

An asynchronous operation is still in progress while a promise is still unfulfilled. promise fulfillment indicates the successful completion of an asynchronous operation. Using the promise constructor, you could make a promise in the following way:

let promise = new Promise(function(resolve, reject) { // create a new promise to resolve or reject}); The constructor function takes a function as an argument. This function is called the executor function.

// Promise constructor as an argumentfunction(resolve, reject) { // doSomethingHere} The executor function takes two arguments, resolve and reject. Your logic goes inside the executor function, which runs automatically when a new promise is created and processes the code as follows:

Step 1: A new promise is created using the promise constructor and two arguments, resolve and reject.

Step 2: The `.then()“ method is called on the promise, which takes two callback functions, one for if the promise is resolved and one for if the promise is rejected.

Step 3: The first callback function will be executed if the promise is successfully resolved, while the second callback function will be executed if the promise is rejected.

Successful call completions are indicated by the resolve function call, and a rejected promise indicates that the asynchronous operation has failed.

Converting a callback to a promiseTo convert a callback to a promise, the first step is to create a new promise object. The promise constructor takes two arguments: a function to which two parameters, resolve and reject, are passed.

When an asynchronous operation succeeds, the resolve function is called, and when the operation fails, the reject function is called. The callback is invoked within this function, and the result is passed to the resolve or reject function, depending on the outcome of the operation. The promise object is returned after the callback has been activated so that it can be used in subsequent processing.

By converting callbacks to promises, developers are able to write code that is more concise and easier to understand.

const incrementDigits = num => { return new Promise((resolve, reject) => { setTimeout(() => { num++; console.log(num); if (num < 10) { resolve(incrementDigits(num)); } else { resolve('done!'); } }, 1000); });};incrementDigits(0).then(res => console.log(res)); Promises are used in this code to carry out an asynchronous operation. Through a technique called promise chaining, promises are a clever way to address issues caused by callback hell.

With less code and simpler techniques, you can use this method to sequentially retrieve data from multiple endpoints. The code above creates a function called incrementDigits() which takes an argument of num (assumed to be a number). It then makes a new promise and passes it two arguments, resolve and reject (both of which are functions).

The next steps are to set a one-second timeout, add one to the value of num, log the new value of num, and, if num is less than 10, resolve the promise and call the function once more with num as an argument. The promise is fulfilled and the string "done!" is logged if the value of num is greater than 10. Finally, the promise is called, and the result is logged.

To manage the promise and return the result of the incrementDigits() asynchronous operation, use the .then()`` method. If an error condition arises inside a promise, you“reject”the promise by calling thereject()function with an error. To handle a promise rejection, you pass a callback to thecatch()` function.

Promise handler methodsThese handlers are simple functions that help to create the link between the executor and the consumer functions so that they can be in synchronization when a promise is resolved or rejected. Instead of just returning the value, however, we return an object that contains this data.

The major promise handler methods are:

  • .then()
  • .catch()
  • .finally()

There are other promise handler methods as well, such as .all(), which creates a single promise that resolves once all of the promises in an array have been fulfilled, and .race(), which creates a single promise that resolves or rejects as soon as one of the promises in an array has been fulfilled, respectively.

Async/awaitIn the section above, we already saw how to create an asynchronous callback.

The use of await, which halts execution until the promise is fulfilled, allows us to write asynchronous functions as though they were synchronous (executed sequentially). Async/await is a technique for building asynchronous code that looks and behaves like synchronous code. It enables you to construct code that appears to run in sequence but operates asynchronously.

This function simplifies the reading and writing of code that employs asynchronous operations. Async/await is a JavaScript technology that allows you to create asynchronous code more synchronously. Similar to callbacks, chaining promises together can become quite clumsy and confusing. For this reason, async and await were developed.

Async functions are defined as follows:

//async/awaitconst newAsyncFunc = async () => {}; The await keyword can then be used inside of this async function to instruct it to wait for something:

const result = asyncFunc();const asyncFunc = async() => { const result = await asyncFunc(); //doSomethingNow} To perform an asynchronous operation, the incrementDigits() function can be written using asynchronous functions and the async/await idiom.

// Example using async/awaitconst incrementDigits = async num => { num++; console.log(num); if (num < 10) { await incrementDigits(num); } else { return 'done!'; }};(async () => { const res = await incrementDigits(0); console.log(res);})(); The async/await syntax is used in this code fragment to increment a number until it reaches 10. The async keyword, which comes before the declaration of the incrementDigits() function, denotes that a promise will be returned by the function.

Then, to halt the incrementDigits() function’s execution until the promise is fulfilled, the await keyword is used. When the promise is resolved, the code following the await keyword will be executed.

Error handling in asynchronous JavaScriptErrors can be coding errors made by the programmer, input errors, or other unforeseeable things.

Error handling in asynchronous JavaScript involves try/catch blocks, which are used in JavaScript to catch any mistakes that might happen throughout the asynchronous process.

try{//...}catch(e){//...} A try statement lets you test a block of code for errors.

A catch statement lets you handle that error. For example:

try{alert("This is an error function!")}catch(e){alert(e)} Including a .catch() method at the conclusion of the asynchronous action will enable the management of any potential errors.

A catch block can have parameters that will give you error information. Generally, the catch block is used to log an error or display specific messages to the user. Now that the `incrementDigits()“ asynchronous function has an error handler function, we can include it as follows:

const incrementDigits = async num => { try{ num++; console.log(num); if (num < 10) { await incrementDigits(num); } else { return 'done!'; } } catch (err) { console.log(err); }};(async () => { const res = await incrementDigits(0); console.log(res);})(); ConclusionIn this post, we explored asynchronous JavaScript, examined the internal workings of the JavaScript asynchronous functions, and learned how to build asynchronous JavaScript using promises and async/await. We have seen that callbacks are simple functions passed to other functions and are only executed when an event is completed.

When dealing with time-consuming processes like network queries or file operations, asynchronous JavaScript is especially helpful. By carrying out these activities in an asynchronous manner, the computer can run other pieces of code while it waits for the results, increasing resource efficiency and speeding up execution.

Asynchronous programming is also essential for handling interactions and events in contemporary web development. It enables programmers to react quickly to user events without causing the user interface to halt/slow.

Asynchronous programming techniques enable the seamless loading of data in the background while the user interacts with the interface, ensuring a responsive and engaging user experience. Asynchronous programming is becoming more and more important due to the rise of single-page applications and the need for smooth user interactions.

For further learning, here are some resources on asynchronous JavaScript.

The post Asynchronous JavaScript for Beginners appeared first on Semaphore.

View Details

Libraries like Locomotive Scroll and React Scroll Parallax are used to enhance your website’s scroll performance. You’ve probably seen lovely animations that appear while scrolling down a website and then noticed that it responds with effects, a customized speed, rotation, and translating objects that take effect. Both libraries will be compared in-depth in this article, along with their benefits and drawbacks, as well as practical usage.

After giving you a glimpse of the scroll animations you frequently see on websites, this article’s primary goal is to compare Locomotive Scroll and React Scroll Parallax for you. Upon reading these articles, hopefully you will want to look into Locomotive Scroll and React Scroll Parallax, which are two frequently used scrolling libraries. It may be worth considering discussing the suitability of the two widely-used scrolling libraries, Locomotive Scroll and React Scroll Parallax.

What is Locomotive Scroll?Locomotive Scroll is a JavaScript library that creates smooth scrolling effects on web pages. It enables programmers to produce visual effects where different elements on a page scroll at different rates to give the impression of depth and dimension. The speed at which elements scroll can also be changed, and easing effects can be added. Locomotive Scroll is a great option because it is lightweight, flexible, and simple to use.

To animate the scroll position of the page, Locomotive Scroll intercepts the browser’s standard scrolling behavior and uses JavaScript. This enables controlled, slick scrolling that can be tailored to produce a range of effects. Locomotive Scroll can be used for parallax effects, where background images move more slowly than the foreground, as well as for other animations like fade-ins and slide-ins. The following is a display from the Locomotive Scroll website, featuring impressive animation effects when scrolling.

The simplicity of Locomotive Scroll’s use is one of its main advantages. The library is a popular option for web developers who want to add scrolling effects to their websites without having to write complex code from scratch because it is lightweight and straightforward to implement. With Locomotive Scroll, it’s possible to design websites with a polished appearance and enticing scrolling animations that grab users’ attention and improve the user experience as a whole.

Notable limitations of Locomotive ScrollDespite the numerous benefits of Locomotive Scroll, there are also several significant limitations associated with it that will be highlighted in this section.

  • Web browsers can interpret Locomotive Scroll’s designs in various ways, leading to inconsistent experiences for users across different platforms.
  • Locomotive Scroll’s compatibility with smaller touch screens can hinder effective content navigation and interaction for users.
  • Screen readers and other assistive technologies may have difficulty working with Locomotive Scroll due to limited compatibility.

What is React Scroll Parallax?A React component called React Scroll Parallax gives web pages visual effects. It enables developers to make a scrolling effect where various page elements move at various speeds based on where they are in the view. This enhances the website’s overall visual appeal by giving the appearance of depth and dimension. An illustration of a display that demonstrates how each element moves at different speeds is shown below.

React Scroll Parallax offers a wide range of options for developers to modify the scrolling behavior, including the speed and direction of the parallax effect. This component is small, simple to use, and works with the majority of current browsers.

React Scroll Parallax causes various elements to scroll at various rates as the user scrolls down the page by changing the position of the web page’s scroll bar. This can be used to produce a variety of effects, such as animated backgrounds, text that fades in and out, and slower-moving foreground images. A website’s user experience can be improved by using this library, which is adaptable and can be used to create a variety of parallax scrolling effects.

Notable drawbacks of React Scroll ParallaxIn this section, we will outline a few common limitations of React Scroll Parallax. However, it’s important to note that these drawbacks are relatively minor and do not significantly impact the usability of the library on our website.

  • Compatibility issues across different browsers and devices
  • Performance problems may arise on devices with low specifications or when a user’s internet connection is slow.
  • Compatibility with older versions of React or other libraries is limited.

The difference(s) between Locomotive Scroll and React Scroll ParallaxYou might find yourself thinking that these two libraries sound very similar, but there are differences. React Scroll Parallax is specifically made for use with React, whereas Locomotive Scroll is a standalone library that works with any JavaScript framework.

Another distinction is that Locomotive Scroll gives programmers more control over the scrolling behavior, including the ability to add easing effects, modify the scroll speed, and so on. On the other hand, React Scroll Parallax is optimized and supports server-side rendering, making it a good option for developing websites that are search engine friendly.

Benefits and drawbacks of both librariesIt’s common to use the libraries Locomotive Scroll and React Scroll Parallax to add parallax scrolling effects to web pages. Following is a summary of some of the two libraries’ potential benefits and drawbacks:

Locomotive ScrollBenefits:

  • Highly customizable, giving programmers more control over how scrolling behaves.
  • It’s ultra-light, which makes it simple to alter and expand its functionality.
  • Compatible with any JavaScript framework and provides support for multiple scroll directions and axes.
  • Offers fluid and natural scrolling effects to improve user experience.
  • Locomotive Scroll enhances brand recognition.

Drawbacks:

  • Performance on websites may be slowed down.
  • The animated scrolling may be overwhelming or distracting to users.

React Scroll ParallaxBenefits:

  • Offers a variety of background images, text, and other elements as well as options for parallax scrolling effects.
  • React is built on top of it, making it simple to integrate with other React libraries and components.
  • Offers a high level of customization through props and configuration options.

Drawbacks:

  • To use effectively requires some familiarity with JSX and React.
  • Could make a React project more complex and burdensome.
  • On older browsers or devices, it might not operate as smoothly.

Comparison between Locomotive Scroll and React Scroll ParallaxWhen deciding which scrolling library is best for your project, comparing Locomotive Scroll and React Scroll Parallax can be helpful. The performance, community, statistical comparison, size and load times, file structure, license, popularity, and learning curve of these two scrolling libraries will all be covered in this section. Because it covers everything you need to know about these two libraries and when to use them, this comparison is significant.

PerformanceDue to the different ways that Locomotive Scroll and React Scroll Parallax are intended to be used, it is challenging to directly compare their performance. However, both libraries are lightweight and optimized, so they shouldn’t have a significant impact on a website’s speed and responsiveness in most cases.

Locomotive Scroll: provides more control over the scrolling behavior, including easing effects and scrollable high speed, which, if improperly used, could have an adverse effect on performance. However, it is still a very effective library that is made to operate without a hitch on contemporary browsers.

React Scroll Parallax: is a performance-optimized solution that works with server-side rendering to cut down on the number of requests made to the server and speed up page load times. Additionally, React, which is renowned for its quick rendering speed and performance, is designed to work well with it.

CommunityDevelopers actively use and contribute to the communities for Locomotive Scroll and React Scroll Parallax.

Locomotive Scroll: has a larger user base and has been around longer, so there are more online resources and examples available. Additionally, the library has a GitHub repository and a dedicated documentation website where users can report bugs and participate in the library’s development.

React Scroll Parallax: is a relatively new library, but React developers are starting to use it more frequently because of how simple it is to use and how well it works with server-side rendering. Users can report bugs and assist in the development of the library by contributing to its GitHub repository. Additionally, there is a Discord channel where users can ask questions and share their experiences as well as a dedicated documentation website.

Statistical comparisonAt the time of writing, Locomotive Scroll had received over 7,935 weekly downloads, On the other hand, React Scroll Parallax has more than 34,696 weekly downloads. These figures imply that both libraries are widely used in web development projects and are well-liked by developers. It is crucial to remember that download counts do not always indicate how good or appropriate a library is for a given project.

Size and load timesCompared to React Scroll Parallax, Locomotive Scroll is a bigger library in terms of size. React Scroll Parallax is about 197 kB, while Locomotive Scroll is about 347 kB. A website’s load times may be impacted by this size difference, especially on slower internet connections.

However, due to their lightweight construction and performance optimization, both libraries should have a negligible impact on load times. The size of the website, the number of assets being loaded, and the speed of the user’s device and internet connection will all affect how long it takes for pages to load in practice.

Even though Locomotive Scroll is bigger than React Scroll Parallax, both libraries are performance-optimized and shouldn’t have a significant impact on how quickly a website loads. The actual load times will depend on several variables and may differ from one website to another.

File structureDepending on the specific implementation and framework employed, the file structure for Locomotive Scroll and React Scroll Parallax will vary, but we can offer some general information.

Since Locomotive Scroll is a stand-alone library, it can be used with any framework or library. It is typically added to a project using a script tag in the HTML file and installed using npm. The library is composed of a single JavaScript file containing the library code and a CSS file containing the library’s default styles. Locomotive Scroll’s file structure is comparatively straightforward, making it simple to incorporate into any project.

In contrast to Locomotive Scroll, React Scroll Parallax is made specifically for use with React and has a different file structure. The components and hooks offered by the library are imported into a React project after the library is typically installed using npm. React Scroll Parallax’s file structure will vary depending on the implementation and framework used, but it typically consists of numerous JavaScript files that contain the library’s code and CSS files that contain the library’s default styles.

LicenseThe MIT License, a permissive license that permits distribution, commercial use, modification, and private use, is the one that governs Locomotive Scroll. Licensed works, modifications, and larger works may be distributed under different terms and without source code, with the only requirements being the preservation of copyright and license notices.

The MIT License, which is the same as the terms of the Locomotive Scroll license, is also the license for React Scroll Parallax, allowing users to freely use, modify, and distribute the software.

The terms of the license stipulate that all copies or significant portions of the software must bear the copyright and permission notices, and it is without warranty.

PopularityAs of February 2023, the NPM registry shows that React Scroll Parallax had been downloaded more than 2.7 million times, while Locomotive Scroll had been downloaded more than 1.5 million times. This implies that React Scroll Parallax may enjoy greater download popularity. React Scroll Parallax and Locomotive Scroll have over 2,346 and 6,205 stars, respectively, on GitHub. Due to the numerous variables that can affect how many stars a project receives, this metric might not be a trustworthy gauge of popularity.

Learning curveDepending on how familiar a developer is with the respective technologies, Locomotive Scroll and React Scroll Parallax have different learning curves.

Any web project, regardless of the technology stack, can incorporate Locomotive Scroll. It has an easy-to-use API and offers thorough documentation and examples on its website. Locomotive Scroll should be simple to learn for developers with JavaScript and CSS experience.

React Scroll Parallax is a library created specifically for use with React. React developers should be able to get up and running quickly, as the library includes a set of components and hooks for creating parallax effects. Developers who are new to React may need to spend some time learning the framework’s basics before using React Scroll Parallax.

Installation/setupInstalling and configuring Locomotive Scroll and React Scroll Parallax will be covered in this section. These two libraries will be used to build two straightforward websites, and installation is not difficult. The website won’t require much work to build, and everything used will be clearly explained.

Locomotive ScrollInstall Locomotive Scroll by running npm install locomotive-scroll to get started using it in standard JavaScript. After installing the library, you must configure it using the following code:

```

``` It’s time for you to learn how to use Locomotive Scroll’s library now that you have configured it.

Use the data-scroll-container attribute to initialize the scrolling effects on your website. This is positioned immediately after or within the body element. The data-scroll attributes must be present in any element that will have the desired effects.

Some attributes are discussed below:

  • data-scroll-direction: this defines the direction of the object for propagation.
  • data-scroll-speed: this defines the speed of the transition. Setting a negative value can make an object move in the opposite direction.
  • data-scroll-class: this works with the default class name coming from Locomotive Scroll, such as appear class which makes an element appear when it reaches the viewpoint.
  • data-scroll-repeat: to repeat a scroll animation in-view.

To learn more about using Locomotive Scroll, click here.

The straightforward website we’ll be building with Locomotive Scroll is shown below. I’ll be pasting the website’s unique custom CSS under the output.

Code:

```

<html lang="en">      <head>                                <title>Document</title>

  </head>

Mountain

Ocean

Thankls for scrolling



</html>

``` The aforementioned code includes the fundamental CSS styles, the actual HTML code that contains the attributes from Locomotive Scroll, and the configuration-related javascript code. By clicking here, you can download the source code. Using Locomotive Scroll, you can also customize the code and add new features to your website.

React Scroll ParallaxInstalling and configuring React Scroll Parallax is something we’ll do in this section. We’ll also talk about how to use it to build a basic website in this article. Run npm I react-scroll-parallax in your terminal to install the library. You must import the library into your React application after installation. Use the code below to import the library:

import { useParallax } from "react-scroll-parallax"; You can use the library once you have imported it, however, let’s first go over the library and how to use the component before we begin writing code. Using React Scroll Parallax, you can create components with objects like translate, scale, rotate, and adjust speed.

To work with React Scroll Parallax, please wrap the App.js component inside the App.js files with the ParallaxProvider component before proceeding:

import React from "react";import ReactDOM from "react-dom/client";import App from "./App";import reportWebVitals from "./reportWebVitals";import { ParallaxProvider } from "react-scroll-parallax";const root = ReactDOM.createRoot(document.getElementById("root"));root.render( <React.StrictMode> <ParallaxProvider> <App /> </ParallaxProvider> </React.StrictMode>);reportWebVitals(); For the benefit of the website, we’ll be using the Parallax component, which consists of the objects scaleX, rotatey, translateX, translatey, and easing. An array, for which it is a little complicated to set the direction of the object, is given to it as a value. However, in order to learn more about these objects from the Parallax component, you must visit the official docs. But before you go to the website to learn more, take a look at the syntax of a straightforward scaleX-based parallax component.

const scaleCParallax = useParallax({ scaleX: [0, 3, "easeInQuad"],}); Below is an output of the simple web page that I created with react-scroll-parallax:

React code:

import "./App.css";import { useParallax } from "react-scroll-parallax";const App = () => { const scaleCParallax = useParallax({ scaleX: [0, 3, "easeInQuad"], }); const parallaxRotateY = useParallax({ rotateY: [0, 360], }); const parallaxEasing = useParallax({ easing: "easeOutQuad", translateX: [-340, 100], }); const parallaxEasingLeft = useParallax({ easing: [1, -0.75, 0.5, 1.34], translateX: [0, -260], translateY: [1100], }); return ( <div> <header> <nav> <li>Home</li> <li>About</li> <li>Contact</li> <li>Services</li> <li>Register</li> </nav> </header> <section className="bg-container"> <img ref={parallaxRotateY.ref} src="https://images.pexels.com/photos/1454360/pexels-photo-1454360.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2" /> <div className="absolute-text"> <h1 ref={parallaxEasing.ref}>WELCOME</h1> <h2 ref={parallaxEasingLeft.ref}>Get Started</h2> </div> </section> <br /> <section className="card-container" ref={scaleCParallax.ref}> <div className="card"> <img src="https://images.pexels.com/photos/1181243/pexels-photo-1181243.jpeg?auto=compress&cs=tinysrgb&w=800" /> <div> <h2>Programming</h2> <p> Coding encourages students to think creatively and outside the box in order to solve problems. They can use their imagination to create new programs, games, or applications that will benefit society. </p> </div> </div> <div className="card"> <img src="https://images.pexels.com/photos/196644/pexels-photo-196644.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2" /> <div> <h2>UI and UX design</h2> <p> Critical thinking and creative problem-solving are required for UI and UX design. Students can use their imagination to create unique, aesthetically pleasing designs that captivate users.. </p> </div> </div> </section> <br /> <br /> <br /> <section className="subscribe-section"> <p>Kinfly create an account to get started</p> <br /> <button>Create an account</button> </section> </div> );};export default App; CSS:

* { margin: 0; padding: 0; box-sizing: border-box;}body { font-family: Arial, Helvetica, sans-serif; background-color: black; color: white;}header nav { display: flex; align-items: center;}header nav li { list-style-type: none; margin-right: 10px; padding: 10px; cursor: pointer;}img { object-fit: cover;}.bg-container { position: relative;}.absolute-text { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);}.absolute-text h1 { font-size: 5rem;}.bg-container img { width: 100%; height: 90vh;}.card-container { display: flex; width: 80%; margin: auto;}.card { flex-basis: 50%; margin: 10px; line-height: 1.6;}.card img { width: 100%;}.subscribe-section { width: 80%; display: block; margin: auto; text-align: center; background-color: tan; padding: 20px;}.subscribe-section button { padding: 10px; outline: none; cursor: pointer;} When to one or the otherNow let’s talk about when to use Locomotive Scroll and React Scroll Parallax. For our front-end project, both libraries are useful, and we need to know when to use one or the other

When to use Locomotive ScrollLocomotive Scroll is particularly useful for websites with single-page layouts or lengthy pages with numerous sections. Users can easily move between the sections of the content on the page without having to manually scroll through each one. Furthermore, Locomotive Scroll works wonders for websites that want to add a modern, stylish touch to their design. It can be used to create distinctive scrolling animations that can help capture users’ attention and keep them engaged with the page’s content.

When to use React Scroll ParallaxReact Scroll Parallax creates the illusion of layers moving at different speeds as the user scrolls through the page, adding depth and dimension to website designs. React Scroll Parallax is especially useful for websites that want to produce an engaging and visually-stunning user experience. It can be used to add a dynamic and interesting element to the design of a website to distinguish it from rival websites. Additionally, websites should use React Scroll Parallax if they want to tell a compelling story. It can be used to create a seamless transition from one area of the page to another, giving the impression that the story is flowing.

Locomotive Scroll Vs React Scroll Parallax: which is better?It’s hard to choose a clear winner as each library has situations in which it might be more appropriate than the other. The character of your project will largely dictate which library is better for your use case. Both libraries are effective resources for building interesting and visually appealing websites, and each has a unique set of features and advantages. React Scroll Parallax may be a better option if you are building a React application and want to add parallax scrolling effects. Locomotive Scroll might be a better choice if you want a standalone library and need more control over the scrolling experience.

ConclusionBoth Locomotive Scroll and React Scroll Parallax have their respective advantages and disadvantages. The best choice will depend on your level of programming expertise and the goals of your project. Both tools might be appropriate to use if you know JavaScript well. You might want to try React Scroll Parallax if you are familiar with Locomotive Scroll and want to improve the scrolling on your website.

The post Website Scroll Comparison: Locomotive Scroll vs React Scroll Parallax appeared first on Semaphore.

View Details

Richard Seroter, Google Cloud’s director of product management and DevRel, shares his perspectives on the importance of learning, the disconnect between technology and business goals, and building customer-oriented teams. In this episode, we will explore his insights on career strategy, the significance of clear goals, the balance between business and tech strategies, and what’s next in Google Cloud.

Edited transcriptionDespite having worked in the software industry for more than two decades, Richard Seroter, Google Cloud’s director of product management and DevRel, assures he has never taken the same job twice. “I think I’ve played all the different things that we deal with in IT, so at least it gives me a bizarre perspective on how it all works together,” He says. Learning from all these different roles resonates with his idea of the strategy for a successful career in the tech industry. Learning itself and learning constantly, believes Richard, is the most important ability you can have. To this extent, he ensures that when looking for hires, he screens for the applicant’s capacity to learn rather than from what they already know.

In this way, Richard’s advice for those in tech is to never stop learning and to learn how to learn. While his passion for learning leads him to meticulously follow a variety of technologies and topics “to at least maintain a basic knowledge on a lot of things”, he knows when “to declare bankruptcy on certain topics” and recognize when to dig deeper on a subject and when to ignore it.

Bridging the gap: Achieving sustainable success by aligning business goals and technology strategiesRichard argues that sustainable, long-standing companies are, most of all, aware of their goals, and consequently have a vision of technology as an enabler to achieve such goals. Hence, they identify with their goals rather than with technology. However, instead of fearing missing out on the latest technologies, they learn to discern when they can pass out of some trends and when to adopt a “fundamental industry shift.”

On the other hand, Richard points out that it is possible to have a disconnection between business goals and technologies. This gap manifests in different ways. For example, management might ignore the use cases of the latest technologies they are unaware of yet with whom engineers are already familiar. Likewise, company executives —or engineers— might want to implement a technology they have heard of for the sake of doing it, yet they only do so on paper: Rather than implementing it, says Richard, they “cheat”: “Can I say I’m doing cloud computing when all I do is move my Hypervisor to the cloud? No, you’re cheating. That’s not using cloud computing. You’re using someone else’s data center. And that still may add value, but unless you’re using managed services, unless you’re doing things that are more composable architectures, you’re building for failure, you’re not doing cloud computing—You’re just renting more expensive servers.”

To bridge that gap of disconnection between technology and its use cases, Richard once again brings up the idea of having a clear vision of your business goals. What’s key, says Richard, is finding the balance between business and tech strategies: You want a mix of tech leaders that also have a business perspective and business leaders that are aware of the technology. In contrast, you do not want people that value only one of these two aspects: “IT teams that only focus on IT metrics never seem to do great, because it doesn’t translate” says Richard, who affirms IT should propose valid reasons for adopting new technologies “If you say instead, ‘look, we have a security issue, we want to make sure that we have a story that protects our customers,’ okay, that’s a good reason.”

Building empathetic teams for enhanced product impactRichard’s mindset for working in IT is keeping in mind that, there’s always a customer, no matter what hat you’re wearing. As such, despite your everyday work might be far from the buyer customer, and you might not receive direct feedback on how they are affected by your product, he believes it is necessary to acknowledge who your customer is: “Even if you’re an SRE, your customer may be the developer. If you’re a developer, the customer may be that application team that needs to use that product.”

From this perspective, Richard assures that you want to build a team with this customer-oriented mindset that reflects how their work impacts the customer, what they enable the customer to do, and what is what the customer wants to do. So you want to create empathetic “teams that can put themselves in the other user’s shoes.”

Richard believes it is the job of leaders to communicate to their teams this empathy aspect and put it into practice. Inviting customers who use their product to meet and chat with the team will make them learn to grasp how much their work affects them, to the extent that Richard says it is important to put a face to their work’s result: “Those are really powerful stories that impacted that human’s life, because of either the good thing or even the bad thing the product did.” Besides, another option for growing empathy in teams is making the team the “customer zero” of the product. If you encourage teams to use their product even before the client does they will internalize it has to be good enough.

Career adviceRichard says that career moves shouldn’t be seen as an inevitable progression. In other words, moving from one position to another isn’t mandatory: “You don’t have to go from developer to architect. You can be an awesome developer for 30 years, and kill it, and that’s amazing.” As there is no natural progression, you might not be aware of what other roles imply, and you might not like some of these other jobs.

In like manner, it is not mandatory to become a manager, Richard believes, and becoming a manager isn’t a reward for you to relax, but a new job that has its responsibilities and that demands acquiring new skills; in this way, “the minute you become more customer-facing in a sales engineer, developer relations, any of these jobs, you also have to become an effective communicator.”

For this reason, keep in mind what kind of communication you are most interested in using. Verbal communication isn’t the only kind of, there are also written, social media, or training communication roles, among others. The idea is communicating so you can help someone achieve their goal-. design a system that acknowledges the difficulties of the customer and shows them how they can achieve their goals. Likewise, acknowledge the audience as is; review how you use technical language and ask yourself whether the audience would understand it or if the communication actually requires it.

What’s Next in Google CloudRichard says that studying end-to-end customer journeys is what drives most cloud providers. As a result, their software tends to be opinionated, that is, it has a predefined way in which it is meant to be used. The trade-off of opinionated software is that it provides clear guidance on how to use the tool: “Google’s been doing some great work on simplifying onboarding, simplifying kind of connective tissue between services, because it shouldn’t feel so intimidating. It shouldn’t feel so overwhelming sometimes.”

Still, Richard points out that cloud platforms should be opinionated but also extensible to not constrain users. In this regard, Google Kubernetes Engine (GKE) is an opinionated version of Kubernetes that follows Richard and his team’s criteria for the right configuration and best practices and that is a great starting point for most users that want to get started quickly. However, it does have the option of changing things for those who want to do it. Creating opinionated software, says Richard, is “taking a point of view, which means there is guaranteed that people won’t like that point of view.” That point of view, says Richard, can come from customer data, looking at what most people are doing, or what they have been making for years.

Again, Richard says that the idea is to have the software oriented toward the goal of the user. “No one wakes up in the morning and says ‘I need to use Kubernetes today.’,” he jokes. Therefore, cloud computing, now and in the future, is not about what you can do, but rather how “can I use these technologies to help me be more agile, help me be more customer-focused, data-driven, delighting people in my user experiences.”

The bottom lineYou can follow Richard on Twitter and Linkedin. Richard also has a blog where he shares daily about what he reads on the web: “I’m interested in a lot of things, just so I at least can kind of be aware of what’s going on.”

The post Richard Seroter on Balancing Business and Technology Strategies appeared first on Semaphore.

View Details

Semaphore Classic has been an integral part of our platform since 2012, and we appreciate the support and feedback we have received from our customers over the years. However, we are excited to announce that Semaphore Classic will be discontinued by September 30, 2023.

We understand that change can be daunting, and we are committed to making the transition as seamless as possible for you. Our Migration Guide will provide you with all the necessary information you need to get started, and our support team will remain available to assist you. If you have any questions or concerns, please do not hesitate to reach out to us at support@semaphoreci.com.

Why is Semaphore Classic being deprecated?Over the past four years, the Semaphore team has invested in building an improved version of our CI/CD platform for our customers. Semaphore 2 is the culmination of that effort, offering new and improved features that significantly expand on what Semaphore Classic provides.

Based on continuous positive customer feedback for Semaphore 2, we have made the decision to focus on providing support for a single version of Semaphore.

Semaphore Classic Deprecation TimelineTo minimize disruption, we will be holding brownouts during September. The table below provides more information about these important events.

| Date | What will happen | | September 4 – 10, 2023 | First Brownout WeekSemaphore Classic will not start jobs for 15 minutes at a time for the given periods:UTC 00:00 — 00:15UTC 10:00 — 10:15UTC 15:00 — 15:15 | | September 11 – 17, 2023 | Second Brownout WeekSemaphore Classic will not start jobs for 30 at a time minutes for the given periods:UTC 00:00 — 00:30UTC 10:00 — 10:30UTC 15:00 — 15:30 | | September 18 – 24, 2023 | Third Brownout WeekSemaphore Classic will not start jobs for 1 hour at a time for the given periods:UTC 00:00 — 01:00UTC 10:00 — 11:00UTC 15:00 — 16:00 | | September 25 – 30, 2023 | Fourth Brownout WeekSemaphore Classic will not start jobs for 3 hours at a time for the given periods:UTC 00:00 — 01:00UTC 10:00 — 11:00UTC 15:00 — 16:00 | | September 30, 2023 | Semaphore Classic end of lifeThe product will be permanently discontinued. |

Does the Semaphore Classic Deprecation Affect me?If you have an organization with active projects on semaphoreci.com, the deprecation will impact you. Please continue reading for more information on how to migrate your projects while maintaining the same pricing plan you currently have. We will guide you through all the key points.

If you are using the Ubuntu 18.04 platform, you are already halfway there. This image selects languages and starts services in the same way as you would in Semaphore 2.

The migration process is a bit more involved if you are using one of the Ubuntu 14.04 platforms, which is why we have created the Semaphore Classic Migration Guide.

However, if the guide doesn’t address your specific use case, don’t worry! We’ve got your back. Please reach out to our support team at support@semaphoreci.com, and we’ll do our best to assist you in migrating your projects.

What about my current pricing plan?If you would like to take advantage of all the new features Semaphore has to offer, you can choose to move to a new pricing plan. Alternatively, if you prefer to keep your current pricing plan, we’ll match it when you create your new organization in Semaphore 2.

Simply send us a message at support@semaphoreci.com, and we’ll take care of the details.

Benefits of Migrating to Semaphore 2Semaphore 2 awaits you, a more modern and powerful platform that surpasses and exceeds what you could achieve in Semaphore Classic:

  • Model any pipelines with a visual workflow editor.
  • A newer generation of machines with top performance to help reduce your development cycles.
  • Monorepo support.
  • Metrics to track down your project performance, speed, and reliability.
  • Artifact store.
  • Have a better overview of your deployment strategies and secrets storage with deployment targets and project secrets

Model your pipelines using Visual Workflow Editor in Semaphore 2.This is just a glimpse of the many new features you can benefit from in Semaphore 2. Furthermore, with Semaphore 2, you will have access to a platform that is continuously evolving to meet your needs.

The post Semaphore Classic Deprecation – How to prepare appeared first on Semaphore.

View Details

Developers and Site Reliability Engineers, or SREs, traditionally work separately and use different tooling to achieve the same outcome of emulating the user workflow in their respective playgrounds. Synthetic Monitoring and End-to-End (E2E) testing are two approaches, owned by SREs and developers respectively, that both emulate the user path. With DevOps aiming to unite these two factions around similar processes to improve application stability, we need to unite around single tooling to validate and monitor the user experience from development through to production.

The reality is that Synthetic Monitoring can serve a means of testing new features and defects throughout the software development lifecycle. As you can see from the below diagram, the E2E testing performed at the development and peer review stages can also be transformed into monitors running against the production application as they both emulate user behavior.

This tutorial makes use of @elastic/synthetics, the Playwright E2E testing and automation framework, and Semaphore and Elastic Synthetics dashboards to show how user journeys can be written together by both SREs and software engineers to validate user workflows and provide regular production monitoring with a single set of tools.

What is Synthetic Monitoring?Synthetic Monitoring is the execution of automated scripts on a regular frequency to determine the availability of a running application. The key goal is to catch potential or ongoing instability issues before users report them.

Synthetic Monitoring involves two types of scripts:

  1. Lightweight monitors, or heartbeats, that will regularly ping an endpoint on a supported protocol such as HTTP, TCP or ICMP, to determine if it is alive. If a response is not received, SREs will want to be notified that the endpoint is unavailable.
  2. Browser monitors are scripts that will execute user interactions such as clicks and text entry against the site to validate that the application is available and able to process user workflows. Depending on how critical a given workflow is to users, SREs will want to be notified of failures or be able to detect performance degradation, using factors such as the time taken to execute the workflow, to preempt software outages.

Both of these monitoring types can help anticipate potential problems with our application in production, and gather useful information for investigating issues.

What is E2E testing?E2E testing is a technique for mimicking user actions with our application, such as button clicks, to validate that their workflow and desired outcome are performed by the software in response to this interaction. E2E tests are typically run as part of local development and within continuous integration, or CI, pipelines to prevent the merging of breaking changes into our main repository branch.

So what is the connection between Synthetic Monitoring and E2E testing? Both E2E tests and browser monitors from Synthetic Monitoring emulate the user behavior and allow us to either test before deployment that the workflow can be executed against the current version of the software, or is still able to be performed against the production version of the application. By executing browser monitors instead of E2E tests, using a tool familiar to both developers and SREs, we can embody the common process ethos of DevOps and build a common understanding between these two groups regarding the features that are exposed to users.

PrerequisitesThe browser monitors generated for production monitoring make use of the Synthetic Monitoring UI, known as Elastic Synthetics. Therefore, before starting, please ensure that the following setup has been completed:

  1. The Elastic cluster is set up either via your local installation or via Elastic Cloud, and Monitor Management has been enabled in Elastic Synthetics.
  2. The Synthetics app has been enabled in the Management tab, as per step 2 of the documented prerequisites.
  3. A note has been made of either the Elastic Cloud instance Cloud ID or on-prem Kibana endpoint to be specified when generating the monitor project using the creation wizard.
  4. A dedicated Project API key has been generated using the Elastic Synthetics application Settings as per the steps given in the Synthetics documentation. This key grants the required permissions for pushing your monitors from your Semaphore CI pipeline.

Generating your first monitor projectOnce the above steps have been completed, the @elastic/synthetics wizard can be used to generate your first monitor project:

npx @elastic/synthetics init my-first-monitor-project The wizard will ask you to provide details including:

  • The Cloud ID or Kibana endpoint of your Elastic cluster.
  • The previously generated Project API key.
  • How often, by default, monitors uploaded to Elastic Synthetics need to run, in minutes.
  • The Kibana space to which the monitors should be uploaded. This is useful if applications are managed by separate teams and the monitors need to be placed in separate spaces to enforce access controls.

The wizard will generate a sample TypeScript project for you with examples to help you get started, similar to the below screenshot:

The key items highlighted include:

  1. The global monitor configuration present within synthetics.config.ts, similar to this example in the demo project. This configuration serves as the default for all monitors within your project and can be overridden in individual journeys.
  2. Sample lightweight monitors, under the lightweight folder. Implemented using YAML-based Heartbeat configuration, these monitors ping the defined endpoint. Details on the configuration options of lightweight monitors are outside the scope of this tutorial.
  3. Example browser monitors located under the journeys folder. These monitors make use of Playwright and TypeScript. It is these monitors that can be used in place of E2E tests to execute and validate user workflow in non-production and production environments.

Understanding monitor configurationThere are 3 key attributes within the `synthetics.config.ts configuration that you may need to change:

  1. params.url denotes the default URL of the site to be monitored. Using these monitors for testing and production purposes means that params.url should be overridden with a new value depending on the environment, as per the example on line 20.
  2. playwrightOptions
  3. monitor, which gives the default settings for all monitors located under the journeys folder.

The monitor configuration contains 3 key settings:

  • The schedule attribute denotes the frequency monitors should be regularly run in Elastic Synthetics. For example, the defined value schedule: 10 will ensure that all monitors will run every 10 minutes unless overridden.
  • locations denote the Elastic Infrastructure locations where the monitors will run.
  • privateLocations references any private locations where you are running your monitors. These should be configured in Elastic Synthetics.

Writing your first user journeyLet’s write our browser monitor to live alongside the generated ones in the journeys folder, hereafter referred to as user journey. A new user journey can be written using the @elastic/synthetics Playwright wrapper and Typescript, or recorded and saved using the Elastic recorder for those less comfortable writing Playwright code. Further examples are also available in this demo project.

Playwright gives you a great API to mimic user actions to reflect the user journey through your application, as well as useful extensions to get elements. The @elastic/synthetics wrapper provides the journey and step wrappers on top of the monitor configuration discussed previously. The benefit of these keywords is they invoke the user perspective and the idea of steps users need to perform to interact with the product. This lends itself to techniques such as Behavior-Driven Development, or BDD, where we write tests in a user behavior format rather than traditional assertion-based unit tests.

An example journey showing the key structure of these monitors is given below:

import { journey, step, monitor, expect } from '@elastic/synthetics';journey('My First Journey', ({ page, params }) => { // Override global monitor configuration for this journey monitor.use({ id: 'my-app-monitor', schedule: 3, }); // Setup our journey before(async () => { await page.goto(params.url); }); // A sample step in the user journey step('assert home page loads', async () => { const header = await page.locator('h1'); expect(await header.textContent()).toBe('My App'); }); step('assert move to login page', async () => { const orderButton = await page.getByTestId('login-button'); await orderButton.click(); const url = page.url(); expect(url).toContain('/login'); const autoLoginOptions = await page.getByTestId('login-options'); expect(await autoLoginOptions.count()).toBeGreaterThan(2); }); // Post-journey events after(() => { console.log('Suite complete!'); });}); The best practice of using the data-testid CSS attribute to segregate your selectors from CSS and JS changes is present in step assert move to login page in the above example. This reduction in test brittleness is recommended in the author’s opinion, which you can see in action through the use of Playwright’s getByTestId selector.

Pushing your monitors within SemaphoreUpon committing your first journey and monitor configuration, it’s time to create a CI pipeline using Semaphore. The getting started documentation gives a step-by-step overview of how to create your first pipeline using the UI.

Like many workflows that developers are familiar with, the first pipeline – Test Synthetics Replicator – builds and tests the website and caches the distribution. This is then deployed later in the second pipeline: Production Deploy Pipeline.

As shown in the above diagram, 4 key steps in the CI pipeline will be discussed in subsequent sections.

1. Build applicationStep 1 builds the application and caches the results, allowing you to validate that your application is building successfully.

The build step looks like this:

sem-version node 19.0.0checkoutnpm installnpm run build Caching the result with the public folder structure in the epilogue allows this version to be used later to deploy the site to Netlify without needing to rebuild the application. Caching them under the folder structure required for our Netlify deployment looks like this:

cache store website-build dist/apps/synthetics-replicator 2. Test using Synthetic MonitorsStage 2 starts a locally running version of the website and executes the monitors programmatically against the site to validate that the suite of user journeys pass. This means we are using the Synthetic Monitors in a similar fashion to e2e testing that developers already use today. Usage of the elastic/synthetics execution is shown below:

sem-version node 19.0.0checkoutnpm installnpm start &cd apps/synthetics-replicator-tests/journeys && npm install @elastic/synthetics && SYNTHETICS_JUNIT_FILE='junit-synthetics.xml' npx @elastic/synthetics . --reporter=junit Within a pipeline execution, Semaphore can pick up XML test result reports to show successful and failed journeys. The documentation shows how to publish the generated file in JUnit format using an epilogue step:

cache store'[[ -f junit-synthetics.xml ]] && test-results publish junit-synthetics.xml' The published report can then be viewed in the Semaphore UI test results tab:

DeployThe application is deployed in step 3 using a previously-cached build to your hosting source such as Netlify. The Semaphore Netlify documentation gives steps to generate the required key and upload it as secret netlify-authentication. Please also ensure that Semaphore is added as a permitted application within your Netlify settings. The full command and configuration for deploying our application looks like the following:

npm install netlify-cli -gcache restore website-buildnetlify deploy --dir=dist/apps/synthetics-replicator --prod Semaphore allows configuration of jobs and promotions to be executed after a pipeline. Conditions are used in the dependent workflow Test Synthetics Replicator to ensure that the promotion is triggered automatically on a successful build from master. The promotion configuration looks like this:

Promote browser monitors to productionStage 4, the last step in our pipeline, deploys our application and pushes the monitors to Elastic Synthetics and will run the monitors from the specified locations against the production application. The commands listed in the Push Monitors step will obtain and push the monitors based on the configuration discussed previously:

npm install netlify-cli -gcache restore website-buildnetlify deploy --dir=dist/apps/synthetics-replicator --prod Note that the API key generated previously as part of the prerequisites must be stored as a secret SYNTHETICS_API_KEY to be used by the push command. Do not make your secrets publicly available in your pipeline configuration!

Pushing any updates to monitors to Elastic Synthetics for monitoring our application in production needs to be done at production deployment time. Pushing at this stage ensures our monitors remain in sync with the latest production site. Having out-of-sync user browser monitors can cause monitor failures that reduce the ability of SREs to monitor and identify issues such as bottlenecks and user errors.

Monitoring production using Elastic SyntheticsPushing the browser monitors to Elastic Synthetics is the connection between developers and their SRE colleagues. Running these monitors regularly every few minutes ensures that SREs can identify failures quickly. They are also able to scrutinize the duration trend of a given workflow to look for potential performance degradation, as illustrated in the screenshot below:

Call to action!As long as developers and SREs are interested in validating or monitoring the state of the user workflow, it’s essential to use common tools whenever possible. Regardless of which domain you belong to, make use of @elastic/synthetics, Playwright E2E testing and automation framework, Semaphore and Elastic Synthetics to both validate user workflows and provide regular production monitoring with a single set of tools.

To find out more, check out the Synthetic Monitoring documentation. Happy monitoring!

The post Synthetic Monitoring Management Using Playwright, @elastic/synthetics and Semaphore appeared first on Semaphore.

View Details

When you begin learning about Kubernetes, you hear about the different types of sets it supports and start wondering about their differences.

Understanding the nuances and distinctions between these key concepts becomes crucial as you navigate the complex — yet rewarding — waters of container orchestration.

In this blog, I am going to go over each type and explain the differences between them, so that you can understand how exactly we use each set, how they differ from each other, and the purpose that each serves.

Prerequisites: A beginner-level familiarity with Kubernetes and its purpose will be enough to understand the details of this blog. This blog’s audience is both expert professionals and beginners. Have fun reading 😄.

Kubernetes ControllersEach of these sets is what’s called a Kubernetes object. More specifically, they’re called Kubernetes controllers. A controller’s job is to monitor the current state of a resource such as pods or services and take action to ensure that the desired state is achieved. Each controller manages a specific type of Kubernetes resource.

Controllers use the Kubernetes API to monitor the state of a particular resource and then perform actions such as scaling, updating, or deleting resources. They are implemented as a loop that monitors for changes in the resource and makes changes if necessary.

The controllers mentioned in this article are built into the Kubernetes system with the difference between them being that each of these solves a unique use case. Let’s start with the first one: the ReplicaSet.

ReplicaSetsIntroductionThe simplest unit in Kubernetes is the pod. We run our containers inside the pod. Say you’ve deployed your app inside a pod and you’re now getting huge traffic. So much that your single pod instance can’t handle it. How do you take care of that? Enter ReplicaSet. A ReplicaSet helps manage traffic by scaling your application to have multiple instances of the same pod. This helps reduce traffic to one particular instance and also helps in load-balancing traffic between each of these instances.

Sample manifestBased on your application’s needs you get to scale the number of instances for your pods easily. Let’s look at what a sample manifest file would look like:

apiVersion: apps/v1kind: ReplicaSetmetadata: name: nginx-replicaset labels: app: nginxspec: replicas: 3 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.14.2 ports: - containerPort: 80 This manifest defines a ReplicaSet for an application called nginx, with three replicas. The pod template specifies a container named nginx using the nginx:1.14.2 image, listening on port 80. The ReplicaSet ensures that there are always three replicas of the pod running and available, labeled with app: nginx.

Let’s look at the spec field inside.

  • .spec.replicas denotes the number of replicas – the number of instances of the pod you would be running. This number would depend on how much you’d want to scale the pod.
  • .spec.selector contains the matchLabels field among others which contains a map of key-value pairs used to match labels on pods. The ReplicaSet will then identify and manage any pods whose labels match the ones specified under this field.
  • .spec.template contains the pod template that is used to create the replicas. These replicas are then managed by the ReplicaSet.
    • .spec.template.metadata contains labels which need to match those under the .spec.selector.matchLabels field for the ReplicaSet to identify said pod.
    • .spec.template.spec contains information for creating the desired pod. It has the container definition, and configuration options such as the volume needed, security context and more.

Here, you can see the output and see that there are 3 different pods under kubectl get pods, denoting the 3 replicas.

In a production-level setting, we wouldn’t be using ReplicaSets. Instead, we would be using something called Deployments. Deployments are preferred in production as they provide more advanced features such as rolling updates, rollbacks and more which allow for easier scaling and management of the application. This makes Deployments help streamline the deployment process and ensure that the deployed application is running smoothly and efficiently.

DeploymentsA Deployment is the preferred way to deploy an application inside a pod. It is a higher-level abstraction built on top of ReplicaSets that uses ReplicaSets internally to manage applications. In addition to the work carried out by a ReplicaSet, it provides added functionality such as:

  • Rolling Updates: Rolling updates ensure that an application is updated gradually, one replica at a time, while ensuring that the overall availability of the application is not impacted. In comparison, ReplicaSets only support scaling and managing replicas.
  • Rollback: Deployments automatically rollback to a previous version of an application if an update fails. For ReplicaSets, this process would need to be manually performed.
  • Version Control: Similar to the previous feature, Deployments implement version control, hence allowing for the ability to rollback to a previous specific version.

Hence for such reasons, Deployments are the preferred way to go as they take care of a lot of the update and rollback functionality without any downtime and ensure that your application stays available and up to date. If you’d like to instead set up custom update functionality, then you could work with ReplicaSets.

Sample manifestLet’s look at a sample Deployment manifest:

apiVersion: apps/v1kind: Deploymentmetadata: name: nginx-deployment labels: app: nginxspec: replicas: 3 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.14.2 ports: - containerPort: 80 This manifest defines a Deployment for the nginx application. It specifies that there should be three replicas of the Deployment and that each pod should be labeled with app: nginx. The .spec section is similar to the ReplicaSet’s, defining the pod template for each replica.

The differences in this manifest are:

  • Using kind: Deployment to state the resource type.
  • The .metadata.name field.

Additional fields in .spec can be used such as minReadySeconds, paused and more. Kubernetes docs talk more about it.

Here you can see how the Deployment itself runs a ReplicaSet that then runs 3 pods.

💡Check out Kubernetes Deployments: The Ultimate Guide to understand more about how Deployments and ReplicaSets are used in a production setting.

StatefulSetNow let’s talk about StatefulSets. What are StatefulSets and why are they needed?

StatefulSet is the controller that manages the deployment and scaling of a set of Stateful pods. A stateful pod in Kubernetes is a pod that requires persistent storage and a stable network identity to maintain its state all the time, even during pod restarts or rescheduling. These pods are commonly used for stateful applications such as databases or distributed file systems as these require a stable identity and persistent storage to maintain data consistency.

A StatefulSet helps manage these pods by providing some key unique features:

  • Unique Identity: StatefulSets assign each pod a unique index, which provides a consistent and unique identity even if pods are deleted or recreated. This is important for stateful applications that rely on a stable identity to maintain the application state or communicate with other nodes in the cluster.
  • Persistent Network Identity: StatefulSets provide each pod with a persistent network identity in the form of a stable hostname based on its ordinal index. This hostname is used for DNS resolution within the cluster, making it easy for other services to discover and communicate with the pods in the StatefulSet, even if they are deleted or recreated.
  • Persistent Storage: StatefulSets provide persistent storage to their pods through Kubernetes PersistentVolumes, which can be dynamically provisioned and attached to pods as needed. This allows stateful applications to store their data reliably across pod restarts and rescheduling, which is important for applications that need to maintain stateful data, such as databases or distributed file systems.

Hence, in comparison to ReplicaSets or Deployments, which are useful for managing general-purpose tools, StatefulSets are used in managing stateful pods that require a unique identity and stable network identity to maintain their state. Take, for example, a database that requires persistent storage. The database’s nodes would maintain their state so that a new node could take over the previous node’s hostname (unique identity) and network identity and hence make sure that data is consistent.

Sample manifestLet’s look at a StatefulSet manifest:

apiVersion: apps/v1kind: StatefulSetmetadata: name: nginx-statefulset labels: app: nginxspec: replicas: 3 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: terminationGracePeriodSeconds: 10 containers: - name: nginx image: nginx:1.14.2 ports: - containerPort: 80 volumeMounts: - name: nginx-data mountPath: /var/www/html serviceName: nginx volumeClaimTemplates: - metadata: name: nginx-data spec: accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 1Gi This is a StatefulSet manifest that manages three nginx pods. The StatefulSet ensures that each pod has a unique identity, a persistent network identity, and a stable hostname. There seem to be additional fields in here. Let’s talk about them:

  • Inside the pod template (.spec.template.spec):
    • terminationGracePeriodSeconds field is used to specify the amount of time after which Kubernetes should be allowed to forcefully terminate a Pod. This allows time for any cleanup task.
    • volumeMounts field specifies which volumes should be mounted inside the container and where in the container they should be mounted. Volumes allow containers to share or persist data between restarts. The volumeMounts field is defined inside the containers field and includes the name of the volume and the path for the volume.
  • .spec.serviceName is used to specify the name for the service that the StatefulSet will manage. This is used to give each pod in the StatefulSet a stable network identity and allows for load-balancing of traffic to these pods.
  • .spec.volumeClaimTemplates is used to define a persistent volume claim that is used by each pod in the StatefulSet, hence providing each pod with its persistent storage. Here, it requests 1GB of storage and uses the ReadWriteOnce access mode.

You can see here the nginx StatefulSet running along with its 3 pods. You can also run the kubectl get pv command to display information about persistent volumes or the kubectl get pvc command to display information about persistent volume claims.

The official Kubernetes documentation page contains more about the different types of fields inside a StatefulSet spec.

DaemonSetLet’s talk about our final set type: a DaemonSet. A DaemonSet ensures that a single instance of a pod is running on each node in a cluster. While the earlier controller types ensure that a specific number of replicas are running across the cluster, DaemonSets are intended to run exactly one pod per node. This is particularly useful for running pods as system daemons or background processes that need to run on every node in the cluster. Due to this, DeamonSets can be used for collecting logs, monitoring system performance, and managing network traffic across the entire cluster.

Let’s talk about a few points about DaemonSets and their difference as compared to the other controller types :

  • Pod Scheduling: DaemonSets are designed to run one instance of a pod on each node in a cluster. If you need multiple instances, then the other controller types would be more useful.
  • Pod Identity: similar to StatefulSets, a DaemonSet runs a unique pod on each node as compared to having unique identities for every pod on each node.
  • Rollout Strategy: since other controller types have multiple instances of a pod, rollouts are done one pod at a time to prevent downtime but as a DaemonSet needs to run a single pod on every node, the rollout process is done one node at a time instead.

Sample manifestNow that we’ve talked about how DaemonSets differ from the previous set types, let’s take a look at a sample DaemonSet manifest.

apiVersion: apps/v1kind: DaemonSetmetadata: name: fluentdspec: selector: matchLabels: app: fluentd template: metadata: labels: app: fluentd spec: containers: - name: fluentd image: fluent/fluentd:v1.7.4-1.0 volumeMounts: - name: varlog mountPath: /var/log terminationGracePeriodSeconds: 30 volumes: - name: varlog hostPath: path: /var/log This manifest runs a single pod of a Fluentd log collector on each node of the cluster. It has a selector to match the label app: fluentd and uses the Fluentd container image.

This manifest should be similar to the ones that we’ve come across. I’ll go over the details in this one:

  • The labels under .spec.selector.matchLabels and .spec.template.metadata.labels should be matching for the DaemonSet to select the fluentd pod.
  • .spec.template.spec field specifies the pod template for the pod to be created.
    • containers specifies the image to be run along with the volume name and its corresponding mountPath. mountPath specifies where in the container would the volume be mounted whereas the type of volume is specified in the volumes field, later.
    • terminationGracePeriodSeconds mentions the time allowed for graceful termination
    • volumes defines the hostPath volume type with the name: varlog. The path field shows the path on the host system which will be mounted on the container.

Here, you can see the DaemonSet running with its single pod instance. This is due to the absence of the replicas field, which signifies a single instance of the pod being run – in our case, running a logging agent.

ConclusionIn conclusion, Kubernetes controllers are used to monitor the current state of a resource and ensure that the desired state is achieved. The controllers mentioned in this article, ReplicaSets, Deployments, StatefulSets and DaemonSets, are built into the Kubernetes system and each solves a unique use case. ReplicaSets help manage traffic by scaling your application to have multiple instances of the same pod, while Deployments provide added functionality such as rolling updates, rollback, and version control. StatefulSets are used to manage stateful applications and DaemonSets ensure that all (or a subset of) nodes run a copy of a pod. Understanding the difference between each set type is useful to understand which one to use in which scenario.

The post Understanding ReplicaSet vs. StatefulSet vs. DaemonSet vs. Deployments appeared first on Semaphore.

View Details

Imagine being a college student with big dreams and even bigger goals. That was me. I was determined to achieve great things and worked tirelessly to make it happen. I landed incredible internships, contributed to major open-source projects, spoke at international conferences, and got the opportunity to be part of many technical communities. It all looked impressive on paper, but deep down, I never felt like I deserved any of it.

Imposter syndrome had taken hold of me, convincing me that I was a fraud and that my accomplishments were all due to luck. It was a constant battle between my achievements and my own self-doubt. I questioned whether I was as smart or talented as people thought I was, and whether I really belonged in the positions I had worked so hard to attain.

But then, I learned I wasn’t alone in feeling this way. Many people struggle with imposter syndrome, despite their accomplishments. Recognizing that it was a common experience helped me to reframe my thinking and be kinder to myself. I started to celebrate my achievements and to trust in my own abilities. It’s not always easy, but working through imposter syndrome has been a journey of self-discovery and growth. Now, instead of always doubting and being hesitant, I feel empowered and confident. And that’s a feeling that I wouldn’t trade for anything.

Understanding the Comparison TrapImage Credits : ccl.orgIf you’re someone who works in the tech industry, you’ve likely experienced the pressure of keeping up with the latest trends, technologies, and skills. You might have felt that you are not progressing fast enough or your skills aren’t on par with others in your field. It’s all too easy to get caught up in comparing ourselves to others, which can be a major hindrance to our own progress and success. But don’t worry, in this blog post, we will explore some strategies for breaking the cycle of comparison and finding fulfillment in your own unique path within the tech world. So, if you’re ready to stop playing the comparison game and start focusing on your own growth and development, read on!

When we constantly compare ourselves to others, we tend to focus on what we lack instead of what we have accomplished. This dangerous mindset can create a toxic work environment and damage relationships with our co-workers and overall well-being. Last but not least, constantly comparing ourselves to others can lead to burnout. If we always strive to be the best and meet unrealistic expectations, we can quickly become overwhelmed and exhausted. But it doesn’t have to be this way.

If I have to be completely honest, I don’t think I ever want to completely let go of Imposter Syndrome. Feeling a sense of humility, intimidation, and even a little bit of fear can be healthy and keep me pushing toward success without getting too comfortable. But at the same time, the comparison trap can be a significant roadblock, causing feelings of depression and a lack of drive. Fortunately, I’ve learned some practical tactics to cope with these emotions and prevent them from spiraling out of control.

Practical tips for overcoming the Comparison Trap1. Celebrate your little accomplishmentsRemember that celebrating your little accomplishments is not just about patting yourself on the back. It’s about recognizing the effort and hard work that went into achieving them. It’s about acknowledging the progress you’ve made and the obstacles you’ve overcome. It’s about taking a moment to appreciate yourself and your journey.

By celebrating your little accomplishments, you’ll also build a positive and resilient mindset that will help you overcome challenges in the future. You’ll be more focused on your own progress and less likely to compare yourself to others. And as you continue to celebrate your small wins, you’ll create a cycle of positive reinforcement that will keep you motivated and inspired.

So, whether it’s completing a challenging project, learning a new skill, or receiving positive feedback from a colleague or client, take the time to celebrate your little accomplishments as a developer. By doing so, you’ll be building a successful and fulfilling career, one small win at a time.

2. Build a support systemI can’t emphasize enough the importance of a support system. Whether you’re starting out in a new job or tackling a new challenge, having people around you who believe in you and want to see you succeed can make all the difference.

In my experience, it’s crucial to have open and honest conversations with your colleagues and managers. They’re not just there to assign you tasks and evaluate your performance; they genuinely care about your well-being and want to see you thrive. And when things get tough, they can be the ones who lift you up and help you find a way forward.

Working in the tech industry can be isolating at times, especially when you’re facing complex problems that require creative solutions. But when you have a support system in place, you’re never alone. You can bounce ideas off of each other, share your struggles and triumphs, and work together to come up with the best solutions.

At the end of the day, effective communication is key to building and maintaining a strong support system. When you’re open and honest about your needs and challenges, your colleagues and managers can be there to offer guidance and support. And when you celebrate your successes together, it’s a win for the entire team.

So my advice to you is this: don’t be afraid to reach out to the people around you. You might be surprised at how much they care and how much they want to help you succeed. And when you work together, there’s no limit to what you can achieve.

  1. Set realistic goalsBeing realistic when setting goals is a lesson that I’ve learned from personal experience. I have often set overly ambitious goals that were difficult or even impossible to achieve. As a result, I became frustrated and discouraged when I didn’t make as much progress as I had hoped. I eventually had to reevaluate my goals and adjust my timeline, which taught me the importance of being realistic when setting goals.

For example, at the beginning of each day, take five minutes to reflect on your ongoing tasks and prioritize them for completion. Don’t overschedule yourself, as this can add unnecessary stress to your day. Instead, focus on accomplishing the tasks at hand and write them down to stay organized.

Breaking down the big tasks into smaller, more manageable ones can make all the difference. It’s easier to make progress and feel motivated when you see your little wins adding up. And let’s be real, who doesn’t love a sense of accomplishment? Below are a few ways to score little wins:

  • Zero Inbox: One of the most satisfying little wins is achieving zero inbox. No more stress from the overwhelming number of unread emails. Just imagine how great it would feel to focus on your work without the nagging feeling that you’re missing something important.
  • Small Fixes: The small fixes like updating the READme file, contributing a tiny fix, filing bugs, and helping someone over slack/discord can help you score little wins to jumpstart your day.

Setting realistic goals have been able to help me to make steady progress in my career and avoid the frustration and disappointment of setting unrealistic goals.

  1. You are not defined by your workThe pandemic brought about a lot of changes, both big and small, in our lives. For me, one of the most significant changes has been in my perspective on work. Before the pandemic, my job was everything to me. I would work long hours and sacrifice my personal life just to make sure things were perfect at work. But then the pandemic hit, and everything changed. I was laid off from my job. This led me to realize that my job was replaceable, but my life outside of work was not. I started using my vacation days for things that really mattered to me. I also started taking better care of myself, focusing on my health and well-being.

Don’t get me wrong, I still take my job seriously and strive to do my best every day. But I’ve learned that my job does not define me. It’s just one small part of who I am. And so, I’ve been trying to find a better balance between work and the other important things in my life. It’s not always easy, and it’s taken some time to get here, but I feel much happier and more fulfilled. Your life outside of work is just as important, if not more so. Take care of yourself, focus on what really matters to you, and find that balance that works for you.

5. We are all togetherAs a developer, it’s easy to fall into the comparison trap. With so many talented individuals in the industry, it can be challenging to feel like you must catch up. But it’s important to remember that everyone’s journey is different. There will always be someone who seems to be doing better than you, but that doesn’t mean you’re not progressing in your own way. Focus on your own growth, learn from others, and celebrate your accomplishments. You are on your own unique path, and the only person you should be comparing yourself to is the person you were yesterday.

The post The Developer’s Dilemma: How to Avoid the Comparison Trap and Build a Fulfilling Career appeared first on Semaphore.

View Details

Today we are introducing a new personalized onboarding process – The Semaphore CI/CD Booster Experience. With this process you’ll be guided by our CI/CD engineers with at least 10 years of experience from day one.

With this new experience we want to provide you the fastest path to:

  • Cut your costs by 50%
  • Improve your productivity at least by a factor of 2x

Our support to our clients has always been exceptional (G2 High Performer) so we are thrilled to introduce this as a complimentary service. By continuously enhancing our customer assistance, we strive to elevate the already outstanding support we have been providing.

How to get started?You simply have to book a call and we’ll take it from there:

Signup now for the Semaphore CI/CD Booster Experience here.

The post Introducing Semaphore CI/CD Booster Experience appeared first on Semaphore.

View Details

Beginners might be intimate by the amount of knowledge and available resources when getting into software development. Finding the right direction and discerning what’s most important can be challenging. On this basis, Tech Writer and DevRel Fernando Doglio shares his insights on the essential skills of a successful software engineer. We’ll delve into his two decades of experience in software development and explore his latest book, Skills of a Successful Software Engineer, as well as his thoughts on software testing and AI-assisted programming.

Edited transcriptionFernando Doglio started as a web developer but moved to different roles over two decades, including data and system architecture. His diverse experience has helped him have a holistic view of how the industry works and has proven handy in his current role as Developers Relations (DevRel) in VoiceMod, a real-time voice changer for games and communication platforms.

As its name suggests, DevRel strategies how to nurture and improve the relationship between developers and the organizations they work with and help them effectively use the company’s products and services. As such, Fernando’s role is building and managing relationships between the company and the developer community, creating a positive experience for developers.

What to expect working in the industryAside from his career as a developer, Fernando has been a prolific writer. His Medium article “9 Hard Lessons I Struggled to Learn During My 18 Years as a Software Developer” granted him great feedback: “There were people actually saying, ‘I wish I had those bits of advice when I started,’ ’’ he recalls. In this article, he compiles what he considers would have been more valuable advice when he got into the industry, namely:

  1. Accepting feedback and being open to better alternatives
  2. Learning multiple languages to enlarge your toolbox
  3. Memorizing algorithms isn’t necessary, but rather the ability to quickly find and implement solutions
  4. Continually learning to stay relevant in the field
  5. Focusing on problem-solving
  6. Optimizing code only after it works
  7. Tackling the small details, even if they are time-consuming
  8. Automate repetitive task
  9. Learning from side projects

The article was well received, and publishing house Manning reached Fernando and encouraged him to turn it into a book now published under the title Skills of a Successful Software Engineer. Rather than a step-by-step guide on becoming a developer, the book “sets the right expectations for the type of career that we have as developers” and specifies “what you should aim for, staying away from the very technical aspects.” In like manner, while the book does provide some tech stack advice and testing advice, Fernando believes it is not as important as learning to “focus on how you write your code and what your aim when you’re writing code and the process to follow.” Besides, the book also offers advice in terms of job interviews and screening companies, everything up to the point of leading a team of developers.

Code testing: Rushing minimum viable product or choosing maximum reliabilityTesting is perhaps the most fundamental skill in software development, Fernando argues, as every developer will rely on testing to ensure the code they produce works. He understands that “we’re all humans and the basic idea of thinking that you’ve covered every aspect of your code and the logic around it and that you know for sure that it’s going to work every time, it’s just foolishness.”

Testing is indispensable for all organizations. Fernando understands that there are companies such as early-stage start-ups that put all their efforts into building a minimum viable product, a product with just enough features to launch and provide feedback. As these companies lack the manpower or funds to ensure code quality, they postpone fixing bugs and setting tests. However, “you can’t go that way because the fact that you don’t have any tests is actively creating technical debt,” says Fernando; as time goes by and your product’s codebase grows, unattended bugs will become ” a huge snowball that’s going to eat you alive,” even if you decide to acquire a quality control team later on.

In this way, tests as a method of quality control are unavoidable. As such, Fernando recommends always setting automated unit tests; he understands that even projects with little time should be put into them since they can make a difference and take little time to set.

AI-assisted programming is the present and the futureDespite its massive use, AI is barely showing signs of what it can do for us. In this regard, Fernando affirms we are witnessing “the birth of the AI-assisted developer” in which developers ask themselves why they should write code if these tools can do it for them. While AI-written code may not work as is, developers can review it, check that it does what it is supposed to, or check if it is in the right direction and correct what’s necessary. Besides, Fernando points out that ChatGPT isn’t the only tool to be used in software development, as other open-source alternatives are available and can be helpful for specific tasks or areas of expertise.

On the other hand, Fernando views AI assistance as a continuation of the declarative programming paradigm, or rather, “a new level above declarative programming”. While imperative programming focuses on describing the specific sequence of actions the computer should take to accomplish a specific task, in declarative programming, the focus is on what the result should be, rather than how to achieve it. Declarative code describes the desired outcome or state and leaves it up to the computer to figure out how to get there. In turn, when working with an AI assistant, says Fernando, “you tell the computer what you want without thinking about code and the code comes your way and now it comes away.” Hence, he believes a developer can use an AI assistant to tackle a problem instead of asking for code, and while “the solution may not be perfect, it will give you some head start, which is also why it’s so useful.”

In this regard, AI assistance also brings up attention to prompting. While AI might be capable of doing a large number of things, “you don’t say, ‘I want to build this application with this and this and this and these are all the requirements I have; just give me the code’, that’s going to be just too much for it,” Fernando explains. Hence, knowing how to prompt is capital, as a new role known as prompt engineering appears.

AI assistance can also play the role of another developer to exchange ideas with and instead of giving you code it gives you suggestions and provides information to challenge your assumptions and make informed decisions. Even in cases where the output code doesn’t work, you can point it out to the AI and it will try to correct it or help you troubleshoot it.

While Fernando is positive about the use of AI, he advises against sharing property software with it, that is, you shouldn’t share with ChatGPT code that you are not allowed to share with anyone outside your company. However, there is a workaround to this; you can prompt about a certain function or problem and the AI will share a possible solution with you even if you didn’t share the code.

Lastly, Fernando remarks that AI is another step in the evolution of software engineering that, as others have done in the past, “allows us to build better software with less work.” To this extent, he affirms that developers have in their hands a tool that allows them to develop software without having to write code anymore: ” The code is the output because it’s needed but I think that probably in a few years we’re not going to be even looking at the code.”

The bottom lineFernando is currently in conversations with Manning to write a new book on the influence of AI in software development, specifically on the tools ”that developers can use to incorporate AI-assisted techniques to their workflow to make sure that they don’t fall behind, to make sure that they can improve their workflow, their performance on the aspect of how fast or how much code they can produce in as little time as possible.”You can follow Fernando on Twitter. If you want to read his articles on software development, business takes, and computer stuff, check his Medium account and subscribe to his newsletter.

The post Fernando Doglio on Skills of a Successful Software Engineer appeared first on Semaphore.

View Details

Recently, the use of passwordless authentication has been rising in popularity among developers. The driving factor for this is that it is more secure and convenient than password-based authentication.

Password-based authentication systems are vulnerable to cyber attacks such as brute-force attacks. To prevent cyber crimes, users must often change their passwords and use difficult-to-guess passwords. Many users may find this to be inconvenient, as they are not good at creating strong passwords.

According to NIST, frequently changing passwords is no longer recommended. As users choose passwords typically relate to their previous passwords and those that are easy to remember. Such passwords are primarily weak and less secure.

Developers must provide a secure account verification architecture to avoid inconveniences and security risks. That’s where passwordless authentication comes into play.

Passwordless authentication is any identity verification method that doesn’t use a password. Getting an in-depth understanding before committing to the approach would be great. This article aims to take you through passwordless authentication and its implementation.

What is passwordless authentication?Passwordless authentication is a more convenient way to log in. It is a verification method that enables users to access applications, systems, and networks without a password. Enterprises and individuals shifting to passwordless authentication is a digital transformation. Thanks to passwordless authentication, passwords are no longer necessary.

Passwordless authentication doesn’t require any particular technology or procedure to verify a user’s identity. It is more of a goal attained by employing various strategies or solutions. Passwordless authentication is frequently used with biometrics, authenticator applications, one-time passcodes (OTP) transmitted by emails or SMS, etc. Passwordless authentication solutions ensure a seamless user experience, improve data security, and lower IT operations costs and complexity.

Passwordless securityPasswordless authentication is more secure than password-based authentication. Secure, in this case, means that the authentication method is less prone to hacking and cyber-attacks. Still, passwordless authentication methods may be vulnerable in one way or another and, thus, are not 100% safe. There may not be an obvious way to hack them, but the most skilled hackers can find a way to get past safety measures, given enough time and effort.

Employing passwordless authentication methods will prevent attacks such as phishing and brute force attacks. Biometric authentication, for instance, offers the highest level of security of any form of authentication. Using biometrics to verify a user’s identity is far more secure than passwords. This makes passwordless authentication hard to beat in terms of security.

Hackers don’t execute complex database attacks to get the user’s credentials. They use social engineering techniques and phishing to steal usernames and passwords. As a result, passwords are the most common target for cybercriminals. Getting rid of them leaves hackers with nothing to steal or manipulate.

To keep passwordless authentication more secure, you can utilize Multi-factor authentication (MFA). MFA provides a more secure way of maintaining the applications, systems, and network.

Passwordless authentication methodsPasswordless authentication is a much more secure and effective way to secure your accounts. This section will break down each authentication method to understand how they can help keep your data safe.

Single Sign-On (SSO)SSO is the most common method of passwordless authentication. It enables users to securely log into third-party applications using a single set of credentials. Almost everyone has logged in using Google, Facebook, Slack, and more via SSO.

SSO prevents users from having to repeatedly authenticate each time they are disconnected from accessing a particular service. Users find this quite convenient as they can retain a valid session on all SSO-enabled applications by identifying themselves once.

BiometricsBiometric authentication verifies a user’s identity using physical or behavioral characteristics. The biological factors include:

  • Fingerprint recognition
  • Facial recognition
  • Voice recognition
  • Iris scan

To set up an account, the user must choose one or more of the above physical characteristics, which are then stored in a database. To access the account, the user’s characteristics are compared with the details stored in the database. Verification occurs when the match succeeds and the user is logged into the account.

Biometric identification is used to simplify a user’s experience with systems. The method is more secure because the probability of identical fingerprints, face, or iris is very low. Various types of applications and devices that are security conscious such as those related to banking, schools, health sectors, mobile phones, and more, use biometric authentication methods to authenticate.

Possession factorsPossession factors grant access to users via something they own/possess, such as a mobile device. Users can receive a one-time passcode (OTP) via email or SMS. Users then get logged into the system automatically by responding to notifications or entering the codes.

Cyberattacks are less likely to occur in this instance because cybercriminals need the relevant possession factor to respond to application’s requests.

Magic linksMagic links allow users to authenticate without a password. The user is prompted to input their email address. The system then sends a one-time URL to the email. Once the URL is opened, the user can log in to the application or account.

Magic links are intuitive and provide a great user experience during authentication. After all, clicking a link and getting direct login to an application is more straightforward than typing a username and password.

Benefits of passwordless authenticationEnhanced user experienceAccording to research by Nordpass, the average person has 70 to 80 passwords. Therefore, it would take the average person a lot of work to create so many complicated passwords. Remembering different complex passwords is also a challenging task. Furthermore, resetting the passwords is a hassle, as much time is wasted. People must go through these inconveniences to log into various applications and keep their accounts secure.

Passwordless authentication provides users with a hassle-free experience. Users can receive an OTP via SMS or email, enter the code, and directly access their account. Biometric authentication requires physical characteristics, i.e. fingerprint or face, to verify and get access to applications. This method is quite secure as physical characteristics are unique in almost all cases. Passwordless authentication improves the user experience because it is fast, efficient, and safe.

Reduced costs and maintenancePasswordless authentication reduces security costs. An organization doesn’t have to spend on password storage systems, management, maintenance, and resets. This saves the IT support department a lot of time and effort because they won’t have to deal with password reset, password recovery, and dealing with lost passwords when a cyber attack occurs. Moreover, theIT team won’t have to find and prevent password breaches.

Passwordless authentication makes it possible to decrease or completely remove the cost of handling password-based security-related concerns. Companies can save millions of dollars by doing away with password-based security and associated password management.

Improved securityPasswords are easily cracked and guessed. Phishing and brute force attacks are the most common methods used to steal passwords. In research done by the Ponemon Institute, companies lose an average of $3.92 million from data breaches. If cybercriminals can get your passwords, it means they are also able to access sensitive company data. Once they have access, they can do what they want with a company’s data.

Passwords are more vulnerable to attacks, and passwordless authentication methods dramatically improve security and reduce data breaches.

Challenges in passwordless authenticationCost of deploymentPasswordless authentication is secure and enhances the user experience. The challenge comes during deployment. You need an in-depth plan during passwordless authentication implementation. Deploying passwordless authentication in organizations attract extra costs. Users may need training, and new software and hardware.

Accepting changeMany people fear change. Some people may be reluctant to do away with the password-based authentication method and try the passwordless authentication method because of this. This requires effort on the part of the organization to overcome, but it’s worth it.

Security constraints in passwordless authenticationPasswordless authentication is not 100% secure. Cybercriminals may use malicious methods such as malware and trojans to breach the security of a system or application. Developers should use strong security measures to avoid cyber attacks. Accordingly, organizations should also use MFA with different levels of security.

Passwordless authentication in practice with Node.jsTo demonstrate passwordless authentication in practice with Node.js. You will need a basic understanding of HTML and JavaScript to create a front end. Also, you will need Node.js installed on your device.

  • You will create a form that collects the user’s email address on the login page.
  • When the users enter and send their email addresses. A magic link is sent. The Node.js server should generate a unique token and send it to the user’s email address along with the link to the login page.
  • When the user clicks the link, the server verifies if the token is valid.
  • If the token is valid, the server sets a session for the user. Finally, the user gets redirected to the application.

Here is the implementation of passwordless authentication using HTML, JavaScript, and Node.js.

Create an HTML form index.html that uses an index.js file.

```

Passwordless Authentication

Enter your email and get a magic link.

```

`index.js`:

const submitButton = document.getElementById("send_email"); const emailInput = document.getElementById("email_address"); let emailValue = ''; emailInput.addEventListener('change', e => { emailValue = e.target.value; }); submitButton.addEventListener("click", handleAuth); /** This function sends a request to the server and sends a magic link to the user.*/ async function handleAuth() { console.log("emailvalue",emailValue); try{ let message = await fetch('http://localhost:3000/send_magic_link',{ method:"POST", body:JSON.stringify({ email: emailValue }), headers:{ "Content-Type":"application/json" } }); message = await message.json(); console.log("message ",message); document.getElementById("info").innerHTML = "Magic link successfully sent!! Hurray!!!"; }catch(error){ document.getElementById("info").innerHTML = "Ooh no! Magic link not sent successfully"; } } Node.js server setupIn this section, you’ll set up the Node.js server. Name the server app.js, also have a .env file that stores the users credentials.

`.env`

EMAIL_HOST="smtp.gmail.com" EMAIL_USER="your_gmail"EMAIL_PASSWORD="your_gmail_password"SECRET="your_secret_key" You’ll begin by creating an express application and installing a few packages. The packages include express, body-parser, cors, nodemailer, jsonwebtoken.

const PORT = process.env.PORT || 3000;const express = require('express');const bodyParser = require('body-parser');const cors = require('cors');const nodeMailer = require('nodemailer');const jwt = require ('jsonwebtoken');const session = require('express-session');require("dotenv").config();const app = express();app.use(cors());app.use(bodyParser.json());app.set('trust proxy', 1) // trust first proxyapp.use(session({secret: 'mySecret', resave: false, saveUninitialized: false})); Create a function for sending the magic linkThe installed packages allow us to handle incoming requests, parse data, and send emails. Because you installed the nodemailer, you will create a transporter to send magic links.

const transport = nodeMailer.createTransport({ service: 'gmail', host: process.env.EMAIL_HOST, port: 587, auth: { user: process.env.EMAIL_USER, pass: process.env.EMAIL_PASSWORD }}); Generating tokensWhen the server receives the email address, It should generate a unique token and store it in a database. The token holds the user’s information, then authenticates the user.

//Generating a token const makeToken = (email) => { const expirationDate = new Date(); expirationDate.setHours(new Date().getHours() + 1); return jwt.sign({ email, expirationDate },process.env.SECRET);}; Users should be authenticated and logged in to the app immediately when they click the link.

// Create email template for magic linkconst emailTemplate = ({ username, link }) => ` <h2>Hey ${username}</h2> <p>Here's the magic link you requested:</p> <p>${link}</p>`;app.post("/send_magic_link", function (req, res) { const { email } = req.body; if (!email) { res.status(403); res.send({ message: "Invalid email address.", }); } const token = makeToken(email); const mailOptions = { from:process.env.EMAIL_USER, html: emailTemplate({ username: email, link: `http://localhost:3000/authenticate_user?token=${token}`, }), subject: "Your Magic Link", to: email, }; return transport.sendMail(mailOptions, (error) =>{ if (error) { console.log("error ",error); res.status(403); res.send("Can't send email."); } else { res.status(200); res.send(`Magic link sent. : http://localhost:3000/authenticate_user?token=${token}`); } });}); Next, we will need an authentication method. The authentication method is called once the token is taken from the front end.

const isAuthenticated = (req, res) => { const { token } = req.query if (!token) { res.status(403) res.send("Can't verify user.") return } let decoded = ""; try { decoded = jwt.verify(token, process.env.SECRET); } catch { res.status(403) res.send("Invalid auth credentials.") return } if (!decoded.hasOwnProperty("email") || !decoded.hasOwnProperty("expirationDate")) { res.status(403) res.send("Invalid auth credentials.") return } const { expirationDate,email } = decoded console.log("decoded ",decoded); if (expirationDate < new Date()) { res.status(403) res.send("Token has expired.") return; } req.session.email = email; res.status(200) return res.redirect('http://localhost:3000/welcome');}app.get('/authenticate_user', (req, res) => { isAuthenticated(req, res);});app.get('/welcome', (req, res) => { return res.status(200).send( "You are logged in! as "+req.session.email );});const server = app.listen(PORT, () => { console.log(`Server running on port ${server.address().port}`);}); When the user clicks the link sent via email, the server should be able to verify the token. This involves comparing it with the secret code in the database. If the tokens match, the user gets authenticated by decoding it using the secret key that created it. If tokens do not match, the system returns an error.

Wrapping upPassword-based login systems are vulnerable and insecure. Migrating to passwordless authentication improves the organization’s user experience, security, and workforce productivity.

Passwordless authentication saves time and costs. Implementing and executing passwordless authentication will cost less than losses from data breaches.

To summarize, you have learned the following;

  • What passwordless authentication is
  • Methods of passwordless authentication
  • Benefits and challenges of passwordless authentication
  • Passwordless authentication in practice with Node.js

That’s it for this overview of passwordless authentication. Happy learning!

The post Developer’s Guide to Passwordless Authentication appeared first on Semaphore.

View Details

Domain-Specific Languages (DSLs) are designed to solve specific problems within an application development domain. DSLs are different from general-purpose languages (GPLs), which are more versatile solutions and solve complex problems in several domains. C#, Java, C++, and Python are examples of general-purpose languages. A DSL focuses on addressing specific challenges faced in a domain. Hypertext Markup Language (HTML) is an example of a DSL. HTML is used to create web pages. Wasp is another, more recent example of DSL used for building full-stack web applications.

Web development is a fast-paced, constantly evolving domain with many new tools and packages popping up all the time. To build a fully functional web application, developers spend time switching between different libraries/frameworks, setting-up deployment configs, and configuring boilerplate. This prolongs the time to get from building to production. Wasp abstracts away these complexities and provides efficient ways for developers to build full-stack web applications.

This article will introduce you to Wasp, an open-source declarative domain-specific language for building and deploying full-stack web applications.

By the end of this article, you will have a good understanding of domain-specific languages and will have learned how Wasp can help you build full-stack web applications efficiently.

What is Wasp?Wasp is a domain-specific language for building full-stack web applications with less code. It abstracts away the complexities of connecting the frontend to the backend, minimizes boilerplate code, and has zero deployment configs. Wasp was created with Haskell, a functional programming language based on mathematical functions. Wasp lets you develop full-stack web applications using React, Node.js, and PrismaDB (PostgreSQL).

There are a lot of intricacies involved in setting up a full-stack application: boilerplates, connecting the client to the server, authentication, deployment configs, data model, routing, database management, etc. These processes are repetitive and time-consuming. They eat deep into development time and prolong the time to get from code to production. Wasp takes care of these intricacies for you so that you can focus more on implementing the details of your application using modern technologies, e.g. React and Node.js (the idea is to support more stacks in the future). The goal of Wasp is to separate web application specifications from their implementation. You specify your application features and concepts in the Wasp language and then carry out implementation details in a familiar stack: React, Node, and PrismaDB.

The Wasp language is straightforward, readable, and has syntax resembling JSON. The description of your application features is written in Wasp and sits inside main.wasp. The implementation of your application is written in React and Node.js. Wasp plans to be stack agnostic in the future, that is, you will have the flexibility to choose the framework you want to build your application with.

Wasp features and how they cater to developers’ needsWasp is a relatively new open-source technology, but features are rolling out regularly. Wasp is currently in Beta with features for building full-stack web applications faster and better. The following features are available in Wasp.

React, Node, and PrismaDB In a single projectThe fundamental characteristic of Wasp is that it allows you to build your application using modern and familiar technologies. It lets you build with React, Node, and PrismaDB all in one project. Your application features and concepts, such as title, routes, authentication, data model, database schema, etc. are written inside main.wasp. Both client-side and server-side code is written inside their respective directories provided by Wasp. The .wasp, React, and Node.js code is compiled into React and Node.js by the Wasp compiler.

The default database in a Wasp project in the development environment is SQLite. However, to move to a production environment, it’s best to change to PostgreSQL. Your database schema, called entities in Wasp, is defined inside the main.wasp file using the Wasp language. Wasp uses the Prisma Schema Language (PSL) to define entities to structure your database.

Let’s define a database schema in Wasp.

main.wasp

entity Roaster {=psl id Int @id @default(autoincrement()) name String isPresent Boolean @default(false)psl=} AuthenticationAuthentication is straightforward in Wasp. Username/password, Google, and GitHub are the authentication methods currently supported by Wasp – other authentication methods will be added in the future. Because the main idea of Wasp is to take care of the complexities of building a web app for you, authentication takes only a few lines of code.

main.wasp

app TestApp{ wasp: { version: "^0.8.0" }, title: "Test App", auth: { userEntity: User, externalAuthEntity: SocialLogin, methods: {google: {} }, onAuthFailedRedirectTo: "/login" },} These few lines of code carry out authentication via Google. GitHub and username/password can also be used for authentication. onAuthFailedRedirectTo specifies that the user be redirected to the /login page if authentication fails.

Native support for optimistic updatesOptimistic updates allow you to reflect changes to the UI while waiting for the server’s confirmation. Let’s take Twitter or Facebook as an example. When a user likes a post, the action is sent to the server. The server then registers it to the database. A response is sent to the client to update the UI. What happens when the user has a slow internet connection? It will take a longer time for the UI to reflect the “like.” These social apps use optimistic updates to update the UI without waiting for the server’s response. Most times, the server returns a successful response, but if it doesn’t (rarely), changes to the UI are automatically rolled back.

Wasp has a built-in optimistic updates feature. A step-by-step process on how optimistic updates work using the “like post” function as an example is shown below:

  • A user clicks the “like” button on a post
  • The client sends the request to the server.
  • The UI is updated to reflect the like without waiting for confirmation from the server.
  • Everything usually goes fine, but on the rare occasion that the server returns “unsuccessful,” the UI update is automatically rolled back.

TypeScript supportWasp has built-in support for TypeScript. Developers who prefer to build with TypeScript (rather than JavaScript) can do so in Wasp. Interestingly, developers can use TypeScript on the client side and JavaScript on the server side or vice versa. Wasp lets you mix and match with no configuration. You could write a file in TypeScript and another in JavaScript. There are no constraints on how to use TypeScript in Wasp.

TailwindCSS supportTailwindCSS is supported in Wasp for styling your web application. By adding two files: tailwind.config.cjs and postcss.config.cjs inside the root of your application, you are ready to style your application with TailwindCSS. Wasp also lets you use plain CSS if that’s your preferred way of styling.

Language Server Protocol (LSP) for VS CodeWasp LSP on VS Code helps you build Wasp applications better by providing IntelliSense, code snippets, auto-completion, syntax highlighting, and error reporting.

Wasp Command Line InterfaceThe Wasp CLI lets you use commands inside the terminal to set up and build your Wasp project. The following are some Wasp commands:

  • wasp new test-project: this command creates a new Wasp project with the name test-project.
  • wasp version: this shows the version of Wasp your application is running on.
  • wasp start: runs Wasp in development mode.

The great thing about Wasp is that it takes care of the complex processes in developing a full-stack application from building to deployment for you. It does the hard work for you under the hood. You can deploy your finished Wasp application to Fly.io with just one command:

wasp deploy fly launch test-project ams This command deploys your application test-project to Fly.io. The ams stands for Amsterdam, Netherlands: the region your app deploys to.

Developer support: Wasp communityWasp has an active community of developers building web applications using Wasp on discord. The community is available to help solve any issues you might encounter while building a Wasp application. Since Wasp is open-source, you can also contribute to the project and provide feedback to the team.

Can DSLs become the future of web development?Domain-specific languages like JSX, Liquid HTML, and CSS have been playing a part in the web development domain for a long time. It is not farfetched to say that DSLs will be a huge part of the future of web development. Web developers are faced with many problems that only micro languages can solve. The reality is that industry experts will continue deriving DSLs (micro languages) from GPLs to solve problems encountered when building modern web applications.

SummaryWasp enables developers to build full-stack web applications efficiently and with less code. It takes care of the complexities like connecting the client to the server, database model, authentication, database management, boilerplate, environment, and configs so that you can focus more on the implementation of details using React and Node.

Wasp has great documentation to get you started with building full-stack web applications. Want to see a working repository with a Wasp application? Check out this Note App built with Wasp.

The post Introduction to Wasp: A DSL for Building Full-Stack Web Applications appeared first on Semaphore.

View Details

Docker Swarm and Kubernetes are both container orchestration technologies. Container orchestration automates the provisioning, deployment, networking, scaling, availability, and lifecycle management of containers.

Container orchestration technologies make it simpler to deploy and manage containerized applications, which makes it easier to scale and run complex applications that have large numbers of containers. Containers enable features like load balancing, service discovery, scalability, self-healing, and rolling updates, which simplify the process of managing containerized applications across many hosts or even various cloud providers.

Some examples of container orchestration technologies include:

  1. Docker Swarm: a native utility for scheduling and clustering Docker containers.
  2. Kubernetes: a native clustering and scheduling tool for Docker containers. Kubernetes is an open-source container orchestration solution that was created by Google.
  3. Apache Mesos: Apache Mesos is a free cluster manager that enables you to execute a range of distributed applications, including containerized apps, on a cluster of computers. Mesos has attributes like fault tolerance, isolation, and dynamic resource allocation.
  4. Amazon ECS: a container orchestration platform from Amazon Web Services that is fully managed.
  5. Google Kubernetes Engine: a Google Cloud Platform-based Kubernetes version that is managed (GCP).
  6. Amazon Elastic Kubernetes Service (EKS) is another popular container orchestration platform from Amazon Web Services that is fully managed and based on Kubernetes. It allows you to easily run, manage, and scale containerized applications using Kubernetes on the AWS cloud.

For the purposes of this article, we will focus on Docker Swarm and Kubernetes, which are the most popular container orchestration technologies.

What is Docker Swarm?Docker Swarm is a container orchestration tool that allows you to manage a cluster of Docker hosts and deploy containerized applications across them.

A comprehensive and flexible system for managing and deploying containerized applications at scale is offered by Docker Swarm. You can benefit from containerization while also guaranteeing that your applications are highly available, scalable, and easy to maintain.

Using Docker swarm, services can be deployed and managed, scaled up or down as necessary, and their performance and health can be tracked. One or more Docker hosts connected into a swarm make up a Docker Swarm cluster. One or more hosts are designated as manager nodes inside the swarm. These hosts are in charge of managing the swarm’s status and organizing the distribution of tasks and services among the worker nodes.

  • Manager nodes: Manager nodes are responsible for managing the Swarm cluster and orchestrating the deployment of services. They maintain the desired state of the services, handle service scaling and rolling updates, and also communicate with other nodes in the cluster to coordinate tasks. For a swarm, Docker recommends having three to seven manager nodes, with five being ideal for most use cases.
  • Worker nodes: Worker nodes are in charge of managing the containers that make up the cluster’s deployed services. They receive tasks in the form of Docker containers from the manager nodes and carry them out. Depending on the workload and resource needs of the deployed services, the number of worker nodes in a Docker Swarm cluster can be scaled up or down.

Below is a diagram of how nodes work in Docker Swarm. Please refer to the official website documentation for a more comprehensive understanding.

Advantages of Docker SwarmDocker Swarm offers several advantages for managing and deploying containerized applications across a cluster of Docker hosts. Here are some of its advantages:

  • Your applications and services can be scaled up or down, based on the workload and resource demands. The workload will be automatically distributed across the available nodes, which lets you add or remove nodes from the swarm as necessary.
  • A highly available and fault-tolerant infrastructure for launching containerized applications is available. If one node fails, Docker Swarm will automatically spread the workload to the remaining nodes, allowing you to run multiple instances of your services across the swarm.
  • A built-in load balancer in Docker Swarm can automatically route incoming requests to the correct service instances that are deployed on the swarm.
  • Rolling updates in Docker Swarm let you upgrade your services and apps with no downtime. With Docker Swarm, the service will be updated one instance at a time while ensuring that it is still accessible.
  • Docker Swarm provides built-in service discovery, allowing your applications to discover and communicate with each other without the need for external service discovery mechanisms.
  • Docker Swarm is simple to install and use because it comes with the Docker engine. In comparison to other container orchestration tools, the API is also more straightforward to learn.
  • Docker Swarm integrates seamlessly with the Docker ecosystem, making it easy to use with other Docker tools and services.

Drawbacks of Docker swarmAlthough Docker Swarm offers a variety of advantages for managing and deploying containerized applications, here are some key drawbacks to consider in Docker Swarm;

  • Compared to other container orchestration tools like Kubernetes, Docker Swarm offers fewer features. Although it offers fundamental tools for scaling as well as managing containers, it might fall short in more complex use cases.
  • There are fewer possibilities for modifying the behavior and configuration of the swarm compared to other container orchestration technologies.
  • The maximum number of manager nodes for Docker Swarm is 7, which might not be enough for large deployments. In comparison to other container orchestration solutions, it also has more constrained scaling capabilities.
  • While Docker Swarm excels at managing Docker containers, its support for other container types is somewhat constrained.

What is Kubernetes?The term “K8s” for Kubernetes, an open-source container orchestration platform, refers to the number of letters between the letters “k” and “s.” Kubernetes automates the deployment, scaling, and management of containerized applications. The Cloud Native Computing Foundation(CNCF) maintains Kuberentes, though it was originally created by Google.

With Kubernetes, you can manage and scale containers across a cluster of hosts and define your application as a collection of containers with all of its dependencies and requirements. A robust tool for operating containerized applications in production environments, it also offers features like automatic rollouts and rollbacks, self-healing, and horizontal scaling.

A master node, which controls the cluster’s general state, and one or more worker nodes, which run the application containers, make up Kubernetes. It is a good practice to have at least three master nodes in a production environment to ensure high availability and fault tolerance. With multiple master nodes, the control plane components are replicated across the nodes, ensuring that the cluster can continue to operate even if one or more nodes fail. The worker nodes run the application containers and connect with the master nodes to receive updates and directives, while the master nodes provide a uniform API for controlling the cluster’s state.

Advantages of KubernetesKubernetes offers a robust and adaptable system for handling containerized applications that helps you in developing and streamlining the application deployment processes, enhancing scalability, and boosting resilience. Below are several advantages of using Kubernetes;

  • Kubernetes offers numerous configuration options, including support for various container runtimes and integration with other tools in the container ecosystem, for deploying and managing applications.
  • Kubernetes can automatically recover from node failures or other disruptions thanks to its built-in high availability and self-healing features.
  • Kubernetes provides a consistent deployment environment for applications, allowing them to be deployed across different cloud providers or on-premises data centers.
  • To manage containerized applications at scale, Kubernetes offers the quick and simple ability to scale applications up or down in response to demand.
  • Rolling updates, load balancing, and service discovery are among the numerous containerized application deployment and management tasks that Kubernetes automates.
  • Kubernetes has a large and active community of developers and users, providing a wealth of resources, support, and tooling.

Drawbacks of KubernetesOrganizations should carefully assess the costs and trade-offs involved in implementing this platform, particularly concerning complexity and resource requirements, even if Kubernetes can offer considerable advantages for managing containerized applications.

  • The learning curve for Kubernetes can be challenging, especially for developers or operators who are unfamiliar with distributed systems and container management.
  • Especially for bigger clusters, Kubernetes can be resource-intensive, necessitating significant computation and storage resources to function well.
  • Although Kubernetes automates many procedures in the deployment and maintenance of applications, it can also add to operational complexity, especially when troubleshooting and diagnosing problems, For example, let’s say that you have a Kubernetes cluster running a web application that is experiencing intermittent connectivity issues. To diagnose the problem, you would need to investigate all the different layers of the application stack, including the pod, container, service, and networking layers, to determine where the issue is occurring. This could involve looking at logs from multiple sources, including the Kubernetes API server, the Kubelet running on each worker node, the application container, and any supporting services or tools.
  • Although Kubernetes works with a wide variety of container runtimes and technologies, some old applications or systems might not function with Kubernetes or need a lot of reconfiguration. For example, in order to migrate a monolithic application that was not designed with containerization in mind to Kubernetes, you would need to break it down into smaller, more modular components that can be containerized.
  • Kubernetes has many security capabilities, but it also necessitates a lot of management and configuration effort to guarantee that the cluster and applications are adequately protected.

Kubernetes vs. Docker Swarm: A comparison

| Comparison | Docker Swarm | Kubernetes | | --- | --- | --- | | Installation | Setup is quick and simple (assuming you already use Docker) | A bit complex in that you must install (and learn to utilize) kubectl | | GUI | No pre-built dashboards but you can incorporate a tool from a third party | Very detailed dashboards | | Scalability | Values growing rapidly (5 times faster than K8s) as opposed to scaling automatically | Scaling based only on traffic | | Learning curve | Simple to use and lightweight, but with limited capabilities | High learning curve but it has more features | | Load balancing | Internal load balancing | No integrated system for automatic load balancing | | CLI | Integrated Docker CLI, which in some use situations may limit the functionality | Needs a separate CLI | | Cluster setup | Simple to begin a cluster | Clusters are difficult to establish, but once they do, they are tremendously powerful. | | Use case | Simple applications that are straightforward to install and manage | Apps with high demand and a complicated configuration | | Community | Active community | Generally well-liked, however, the user base is shrinking |

Installation and setupLinux, macOS, and Windows are just a few of the operating systems on which Kubernetes and Docker Swarm can be installed. A cloud provider, a Kubernetes distribution, or manually establishing a cluster using kubeadm are just a few of the installation options offered by Kubernetes. Users must create a cluster of nodes that run the Kubernetes software after the program has been installed. For managing and deploying containerized applications, Kubernetes provides several tools, like kubectl.

In contrast, Docker Machine, Docker Compose, or manually building up a cluster using Docker Swarm commands are just a few of the installation options offered by Docker Swarm. Users of Docker Swarm must additionally build up a swarm of nodes running the Docker Swarm program. For delivering and administering containerized applications, Docker provides several tools, including docker service and docker stack.

Generally, the installation and setup procedures for Kubernetes and Docker Swarm are not comparable. For the deployment and management of containerized applications, Kubernetes provides extra install options and more advanced tooling while Docker Swarm provides clearer tooling options with an easier installation.

PopularityDue to its open-source status, community support, industry adoption, extensibility, and feature-rich offerings, Kubernetes has grown in popularity since it was first released by Google in 2014. In many industries, Kubernetes has replaced other container orchestration systems as the standard.

On the other hand, Docker Swarm has experienced a fall in popularity in recent years as a result of its limited feature set, weaker community support, and lower level of industry acceptance. While some businesses continue to utilize Docker Swarm, Kubernetes has become the most well-liked and used container orchestration software.

In summary, Kubernetes and Docker Swarm have their strengths and weaknesses, Kubernetes has gained more popularity and industry adoption, while Docker Swarm has become less popular in recent years.

Load balancingUsing the Kubernetes Service resource, Kubernetes provides built-in functionality for load balancing. A set of pods can have a consistent IP address and DNS name thanks to Kubernetes Service, which can also route inbound traffic to those pods. Several load-balancing methods, such as round-robin, session affinity and IP hash, are offered by Kubernetes Service. It also interfaces with outside load balancers to offer more sophisticated load-balancing functionalities.

Docker Swarm provides built-in support for load balancing. Docker Swarm Mode uses an ingress routing mesh to route inbound traffic to the correct service within the swarm. Several load balancing methods, such as round-robin and least connections, are used by Docker Swarm Mode. To offer sophisticated load-balancing features, Docker Swarm may also integrate with external load balancers.

Typically, load balancing is integrated into both Kubernetes and Docker Swarm, albeit the algorithms they use to distribute incoming traffic differ. In comparison to Docker Swarm, Kubernetes Service has more sophisticated load-balancing features and can integrate with a wider range of external load balancers.

ScalabilityKubernetes is made to horizontally scale applications, new pods can be added to a deployment or replica set to adapt to an increase in workload or traffic. To dynamically scale up or down applications in response to demand, Kubernetes offers a number of tools, including the Horizontal Pod Autoscaler (HPA), Cluster Autoscaler, and Vertical Pod Autoscaler (VPA). Kubernetes is appropriate for large-scale deployments since it can scale over several nodes.

Moreover, Docker Swarm is made to horizontally scale applications, so it can deploy more replicas to a service to manage increased workloads or traffic. Docker Service Scale, a scaling tool offered by Docker Swarm, enables users to scale services up or down in response to demand. Docker Swarm is suitable for massive deployments since it can scale over several machines.

Overall, both Kubernetes and Docker Swarm provide horizontal scaling capabilities and can scale across multiple nodes. However, Kubernetes offers more advanced scaling tools, including HPA, Cluster Autoscaler, and VPA, making it more suitable for complex and demanding workloads.

NetworkingThe flat network architecture used by Kubernetes allows containers to connect with one another and with services within the cluster. Kubernetes offers a strong and flexible networking approach. ClusterIP, NodePort, and LoadBalancer are just a few of the networking options that Kubernetes provides to expose services and transport traffic across pods. In order to manage traffic within the cluster and interface with other networks, Kubernetes additionally offers network policies. See the documentation here.

Calico, Flannel, and Cilium are just a few of the network plugins that Kubernetes offers to add extra networking features including enhanced security policies, multi-cluster connectivity, and service mesh functionality.

Docker Swarm offers a flexible and scalable networking approach that makes use of an overlay network to let containers communicate with one another across various swarm nodes. To route communication between services and containers, Docker Swarm provides a variety of networking options, such as ingress and overlay networks.

With the help of a DNS-based load-balancing mechanism, Docker Swarm provides built-in support for service discovery and load balancing. This method divides incoming traffic among service replicas.

Overall, robust networking capabilities and a variety of options for network design and control are offered by both Kubernetes and Docker Swarm. However, Kubernetes is better suited for complicated and demanding workloads since it provides a wider range of networking options and enables more sophisticated network plugins.

High availabilityThe API server, controller manager, and scheduler are three master node components of Kubernetes that offer high availability. These components are kept highly available by Kubernetes via a variety of techniques, including replication and clustering.

Kubernetes stores cluster state data in Etcd, a distributed key-value store. By duplicating data over several cluster nodes, Etcd offers high availability, guaranteeing that the cluster state is always accessible even in the event that some nodes fail.

Via Docker Swarm mode design, which consists of management and worker nodes, Docker Swarm offers high availability. By replication and clustering, Docker Swarm assures high availability of the manager nodes. Docker Swarm additionally uses a distributed key-value store to store cluster state information, guaranteeing that the cluster state is always available even if some nodes fail.

PerformanceKubernetes has been tested to scale up to thousands of nodes in a cluster and is renowned for its capacity to manage large-scale deployments. It provides cutting-edge features like rolling updates and canary deployments, enabling updates with zero downtime and optimum resource utilization. Moreover, Kubernetes features a strong network architecture with integrated load balancing and cutting-edge networking plugins like Calico and Flannel.

Docker Swarm is recognized for being straightforward and user-friendly. It is an excellent option for smaller deployments because it has a smaller footprint than Kubernetes. Load balancing and service discovery capabilities are also included in Docker Swarm, making it simple to deploy and operate containerized applications.

Both Kubernetes and Docker Swarm have demonstrated strong performance in a number of tests when it comes to performance benchmarks. The precise performance measurements, however, may change based on the use case and workload being tested.

DeploymentApplications are deployed using Kubernetes as a collection of containers that are controlled by a deployment, which is a higher-level object. The application’s desired state, including the number of replicas, the container image to use, and other configuration options are specified in the deployment object. Even in the face of failures or changes in demand, Kubernetes automatically manages the deployment to guarantee that the desired state is maintained. See specific information here.

Applications are deployed in Docker Swarm as a collection of services that define the state that the application should be in, as well as how it should be duplicated and load balanced across the swarm. Similar to Kubernetes, Docker Swarm offers automatic failover and self-healing features to keep the application responsive and available. See official documentation.

DNS-based service discoveryDNS-based service discovery allows services to be accessed by their name rather than their IP address, which makes it easier to manage and scale applications in a dynamic environment.

The Kubernetes DNS service, which is a platform feature, offers DNS-based service discovery in Kubernetes. Each service and endpoint in the cluster receives an automatic DNS entry from Kubernetes, and other services or applications can use the Kubernetes DNS service to resolve these entries.

SwarmKit, an embedded DNS resolver in Docker Swarm, offers DNS-based service discovery. Each service and job in the swarm receives a DNS entry from SwarmKit, and these entries can be resolved by other services or programs using the integrated DNS resolver. See more here.

Using their respective DNS-based systems, Kubernetes and Docker Swarm both enable load balancing and automatic service discovery. Nevertheless, Kubernetes has more sophisticated features like DNS policies that let administrators alter the way DNS resolution occurs. Although Docker Swarm implements DNS-based service discovery more simply, it may be simpler to handle in smaller or less complex setups.

Graphical User Interface (GUI)Kubernetes features a web-based dashboard for its graphical user interface (GUI), which offers a visual depiction of the cluster’s state and enables users to control and monitor their applications. Plugins and extensions can also be used to personalize the interface. However, although Docker Swarm lacks a native GUI, there are third-party tools like Portainer and Docker Universal Control Plane that may be used to manage and monitor Docker Swarm clusters. The decision between Kubernetes and Docker Swarm for GUI ultimately comes down to personal preference and the level of control and visualization needed for the user’s containerized applications.

Docker Swarm vs Kubernetes: which works better?

The choice between Docker Swarm and Kubernetes ultimately depends on the specific needs of the user’s containerized application environment. Both platforms have their strengths and weaknesses.

Docker Swarm is a suitable option for small to medium-sized deployments because it’s easier to set up and administer. In addition, it is easier to understand and uses fewer resources than Kubernetes to operate.

Kubernetes, on the other hand, is more sophisticated, has a steeper learning curve, and offers a greater degree of flexibility and scalability, making it better suited for larger and more complicated installations. Kubernetes has a larger and more active community than competing technologies, which leads to a greater variety of available resources and third-party integrations.

ConclusionNo matter which platform you use, it is crucial to thoroughly weigh your alternatives and pick the one that best suits your needs. The smooth and secure operation of your deployment will also require regular maintenance and updates. For better insight, see the official documentation.

References:

  • https://docs.docker.com/get-started/swarm-deploy/
  • https://kubernetes.io/

The post Docker Swarm vs. Kubernetes appeared first on Semaphore.

View Details

Continuous Integration and Delivery (CI/CD) is fundamental to modern software development, enabling teams to deliver high-quality code — reliably and at a fast pace.

As CI/CD becomes widespread in the industry, developers that know the ins and out of Continuous Integration (CI) become more valuable and productive.

But with so many resources available online, it can take time to figure out where to start. That’s why we have developed Semaphore’s CI/CD Learning Tool.

You can see it in action here:

You can try it right now and if your like it leave a review on Product Hunt so more people discover it.

A learning path just for youThe CI/CD Learning Tool can help you set your own learning objectives, guiding you with a personalized way to achieve your goals. Whether you’re a beginner looking to understand the basics of continuous integration or a senior engineer looking to enhance your skills, you will discover ways to improve your productivity as a developer.

The journey starts by going to the CI/CD Learning Tool and selecting the learning track you wish to focus on:

  • Continuous integration: involves testing the sanity of your codebase by building and testing it on every change. That way, you can answer things like, “Is my project buildable?” “Does it pass all the tests?” “Does it have any security vulnerabilities?” at all times.
  • Continuous delivery: takes the output of CI and builds a releasable package, such as an executable file, an installable library, or a Docker image.
  • Continuous deployment: deploying the application into the world is the last leg of a CI/CD workflow.

Select one of the three learning tracksSetting your goalsOnce you select a learning track, you can begin setting your objectives. The tool will then build a personalized learning path tailored to your specific goals.

What do you want to achieve?Your learning journeyThe tool will adjust your learning path as you select your goals, providing a step-by-step guide to CI/CD skills to master.

The skills that will get your there.For more context, the tool provides a suggested pipeline that uses the newly-acquired skills to the maximum.

A reference pipeline that achieves your goals efficiently.Subscribe for personalized learningBecause learning takes time, you can optionally subscribe to receive extra content on any of the learning tracks. If you do, you will receive a ton of customized content for free to help you achieve your goals. Subscribing is optional and not required to use the learning tool.

Subscribe for more contentGive it a tryThe CI/CD Learning Tool allows everyone to take control of their learning, enhancing their skills at their own pace. Whether you’re starting or looking to take your skills to the next level, we’re heart to help you achieve your goals.

Start your learning journey today

We hope you have as much fun using it as we did making it. And if you liked it, it would be awesome if you could leave a review on Product Hunt to help get more visibility.

The post Announcing A New Way of Learning: The CI/CD Learning Tool appeared first on Semaphore.

View Details

Testing is essential to the software development cycle, and in ensuring code integrity, reliability, and quality. As the complexity of software systems increases, testing is crucial to identify bugs and errors that may lead to system failure. Go provides the testing package for testing, however, writing tests with it can be time-consuming and tedious. Fortunately, the Go ecosystem is home to sophisticated third-party packages like Testify that make test-related tasks more intuitive.

Testify is a popular testing toolkit for Go that provides a set of utilities and assertion functions that simplify testing. Testify extends the built-in testing package, allowing Go developers to write comprehensive tests confidently with features like assertions, mocks, suites, and others.

Getting Started with the Testify PackageGetting started with the testify package is easy. You’ll need to install the package in your Go project and write the tests in your Go test files.

First, initialize a new Go project with the go mod init command.

go mod init Next, run the following command in the terminal of your project’s directory to install the testify package using the go get command.

go get github.com/stretchr/testify You can set up the Testify package in your Go project by importing the package into your test files.

import "github.com/stretchr/testify" You’ll need a Go test file to run tests with the testify package. Typical Go test files end with the _test.go suffix appended to the file name where the functions you’re testing are located.

Run the following command in your project’s working directory to create a Go test file for this tutorial.

touch examples_test.go You can run all the tests in your Go project with the test command. Adding the -v flag returns a verbose output of the test.

go test -v The testify package provides packages for multiple functionalities. You’ll use packages like assert and mock for assertions and mock testing, along with other packages in the testify package.

Assertions with TestifyAssertions are statements that validate certain conditions to test the behavior of a function or a piece of code. In Testify, the assert package provides various assertion functions for various test cases.

Here’s a function that adds two numbers and returns the sum and an error type.

package mainimport "fmt"// MyFunction returns the sum of two integers.func MyFunction(a, b int) (int, error) {if a < 0 || b < 0 {return 0, fmt.Errorf("both arguments must be non-negative")}return a + b, nil} The MyFunction function takes in two integers. If any of the integers is a negative number on the function call, the function returns 0 and an error with the fmt package’s Errorf function.

Here’s how you can test the MyFunction function with the testify package’s Equal, NoError, and Error functions.

package mainimport ("testing""github.com/stretchr/testify/assert")// In your Go test filefunc TestMyFunction(t *testing.T) {// Test case 1: Positive inputssum, err := MyFunction(2, 3)assert.Equal(t, 5, sum, "Sum of 2 and 3 is 5")assert.NoError(t, err, "There are no errors")}// In your Go test filefunc TestMyFunction2(t *testing.T) {// Test case 2: One negative inputsum, err := MyFunction(-2, 3)assert.Equal(t, 0, sum, "Sum of -2 and 3 is 0")assert.Error(t, err, "Error is not nil for negative input")} The functions are typical Go test functions that take instances of the *testing.T type for the tests.

The TestMyFunction test function calls the MyFunction and uses the Equal function of the assert package to assert equality between the result and 5. The NoError function of the assert package asserts that a function has returned no errors.
Similarly, the TestMyFunction2 test function calls MyFunction and uses the Equal function of the assert package to assert equality between the result and 0. The Error function of the assert package asserts that a function has returned errors.

Both test functions should pass since the MyFunction function returns the same result as the edge case in both scenarios.

Mocks with TestifyMocks are objects that simulate the behavior of real objects. Mocks are useful for testing interactions between different parts of a system. The mock package provides functionality for mock testing in the testify package.

Here’s how you can create a simple mock object for testing.

package mainimport ("testing""github.com/stretchr/testify/assert""github.com/stretchr/testify/mock")type mockCalculateArea struct {mock.Mock}func (m *mockCalculateArea) calculateArea(width int, height int) int {args := m.Called(width, height)return args.Int(0)} After importing the mock and assert packages, we declared the mockCalculateArea struct that embeds the Mock type from the mock package for the calculateArea function.

The calculateArea function implements the mockCalculateArea struct. The args variable holds the arguments passed to the mock object (the width and height arguments). Then the Int function takes in zero and extracts the first integer value from the mock.Arguments object, and returns the value as the result of the mock function.

Here’s how you can create mock objects for your test functions.

// In your Go test filefunc TestCalculateArea(t *testing.T) {// Create a mock object for the calculateArea functionmockObj := new(mockCalculateArea)// Set up the expected return valuemockObj.On("calculateArea", 5, 10).Return(50)// Call the function and check the resultactualArea := mockObj.calculateArea(5, 10)assert.Equal(t, 50, actualArea, "The calculated area is incorrect")// Verify that the expected function call was mademockObj.AssertExpectations(t)} The TestCalculateArea function uses the mockCalculateArea mock object to test the calculateArea function. The new function creates a new mock object of the mockCalculateArea type to simulate the behavior of the calculateArea function during the test.

The TestCalculateArea function sets the expected return value of the calculateArea function using the On method of the mock object that takes the expected input values for the function call as arguments (5 and 10 in this case) and the expected return value (50 in this case). The calculateArea method calls the mock object with the same input values (5 and 10) as the expected function call. The actual return value of the function is stored in the actualArea variable.

The assert.Equal function verifies that the actualArea variable equals the expected return value (50 in this case). If the actual value doesn’t match the expected value, the test fails, and a message is printed to the console.

Finally, the function calls the AssertExpectations method of the mock object to verify that the test made the expected function call during the test. If the function wasn’t called with the expected input values, it fails.

Test suites with TestifyTest suites are collections of test cases designed to test the functionality of a specific software application, system, or component. Test suites typically include a set of test cases executed concurrently in a predefined order to achieve a specific testing goal.

You’ll need to define a struct that embeds the Suite type of the suite package to define test suites. The functions for the test suite will implement the struct type that embeds the Suite type.

package mainimport ( "testing" "github.com/stretchr/testify/suite")// Define a test suite struct that embeds suite.Suitetype MySuite struct { suite.Suite myVar int}// Define a SetupTest method that will be called before each testfunc (s *MySuite) SetupTest() { s.myVar = 42}// Define a TearDownTest method that will be called after each testfunc (s *MySuite) TearDownTest() { // cleanup code goes here}// Define individual test functions that will be run by the suitefunc (s *MySuite) TestSomething() { // use s.Assert() or s.Require() to make assertions s.Assert().Equal(42, s.myVar)}// Define another test functionfunc (s *MySuite) TestSomethingElse() { s.myVar = 0 s.Require().NotEqual(42, s.myVar)} MySuite is the struct that embeds the Suite function for the test suite and the methods that implement the MySuite function define various test functions.

You can run all the test suites with the Run function of the suite package.

// In your Go test filefunc TestMySuite(t *testing.T) { suite.Run(t, new(MySuite))} The run function takes the testing instance and an instance of the test suite struct, and runs the methods that implement it.

Test hooks With TestifyTest hooks are special functions that execute before or after running tests. You can use test hooks to set up test fixtures, perform clean-up tasks, or customize test behavior.

Here’s how you can implement test hooks in your Go programs.

package mainimport ("testing""github.com/stretchr/testify/assert")// In your Go test filefunc TestExample(t *testing.T) {// Set up a "before" hook that will run before the test.t.Run("before", func(t *testing.T) {// Do some setup work here.})// Set up an "after" hook that will run after the test.t.Run("after", func(t *testing.T) {// Do some cleanup work here.})// Write the actual test code here.t.Run("test", func(t *testing.T) {// Use assertions to check the expected results.assert.Equal(t, 1+1, 2, "1+1 must be equal to 2")})} The TestExample function is a test function containing three nested test functions: before, after and tests, created with the Run method of the test instance that creates subtests that you can run independently from other tests.

The before and after functions are test hooks that get executed before and after the test function, respectively. You can perform any operations inside these functions. In the test function, the Testify assert package makes assertions on the expected behavior of the code. In the previous example, we used the assert.Equal method to check that 1+1 equals 2.

TDD vs. BDD with TestifyRecently, two popular testing methodologies have garnered popularity, namely Test Driven Development (TDD) and Behavior Driven Development (BDD). TDD and BDD are both used to ensure that software meets certain requirements and functions as intended.

TDD is a development approach where you write tests before writing the actual code. The idea behind TDD is to write small, testable units of code that meet the requirements of the software. After writing the tests, you write code that passes the tests.

TDD is all about writing tests that drive the development of the code. This approach ensures that the code meets requirements and is of high quality.

BDD, on the other hand, is a development approach focused on the behavior of the software. With BDD, you write tests that describe the expected behavior of the software. The tests are usually written in a specific syntax called Gherkin, designed to be intuitive for both technical and non-technical stakeholders. BDD is all about describing the behavior of the software in a way that is understandable to everyone involved.

How Testify supports TDD and BDDTestify supports both TDD and BDD testing methodologies. The suite of testing tools that the Testify package provides makes it easy to write tests that meet the requirements of the software.

For TDD, Testify provides tools for writing unit tests. These tools include assertions, mocks, and test suites. With Testify, developers can write tests that ensure that their code meets requirements and behaves as expected.

For BDD, Testify provides a Gherkin-style syntax for writing tests, designed for easy readability by both technical and non-technical stakeholders. Testify’s BDD tools also include assertions, mocks, and test suites. With Testify, you can write tests that describe the expected behavior of your software in a way that’s understandable for everyone involved.

| Topic | TDD with Testify | BDD with Testify | | --- | --- | --- | | Golang Testify Package | Provides various testing functions and assertions for writing tests in Go | Supports both TDD and BDD methodologies by providing assertion functions and the suite.Suite interface for defining test suites | | Purpose | Emphasizes on testing the functionality | Focuses on testing the behavior | | Test Structure | You write test cases for each function | You write test cases for each behavior/scenario | | Syntax | Uses assertions for checking the expected results | Uses natural language statements (Given-When-Then format) for describing test cases | | User Involvement | Primarily for developers | Involves developers, testers, and business stakeholders | | Collaboration | Minimal Collaboration | Engages collaboration between developers, testers, and business stakeholders | | Test Documentation | Requires use of documentation to explain the code logic | You create Documentation using natural language to describe code behaviour |

The table serves as a comparison between both methodologies and isn’t exhaustive. Testify provides functionality for both TDD and BDD and you can use the package for either approach.

Benefits of using TestifyMost Go developers choose to Testify for testing their applications due to the many benefits, features, and functionalities that the package provides. Here’s an overview of the benefits you access when using the Testify package.

  1. It allows you to define subtests for testing different scenarios within a single test function. Subtests make it easy to group related test cases together and improve test organization.
  2. The library provides functionality for colorful and readable output formatting that can make it easier to identify and fix test failures. Outputs like helpful error messages, stack traces, and logs are all presented in an easy-to-read format.
  3. Testify provides a wide range of assertion functions that you can use to write concise and clear test cases. Using assertion functions, writing tests that are both simple and expressive is easy, reducing the amount of boilerplate code.
  4. It includes a mock package for creating mock objects and functions for testing. The mock package is especially useful when testing code that has external dependencies, as it allows you to isolate the code from its dependencies. The mock package is easy to use, and it can help you save a significant amount of time when writing tests.

Testify vs GoConveyTo fully understand the benefits of using the Testify package, you can compare it to other popular testing packages like the GoConvey package. The GoConvey package is a testing framework that provides a high-level API for defining test suites and test cases, including a web-based user interface for viewing and analyzing test results.

Here’s a comparison table comparing the Testify and GoConvey packages.

| Feature / Benefit | Testify | GoConvey | | --- | --- | --- | | Assertion Functions | Testify provides a wide range of assertion functions that you can use to write concise and clear test cases. These include functions like assert.Equal, assert.NotEqual, assert.True, assert.False, and many more. | GoConvey also provides a range of assertion functions like So, ShouldBeEqual, ShouldNotBeEmpty, and others, but the list of assertion functions isn’t as extensive as Testify. | | Mocking | Testify provides a mock package that you can use to create mock objects and functions for testing. | GoConvey does not provide built-in mocking capabilities. | | Suite Setup and Teardown | Testify allows for set up and tear down test suite fixtures, which can save you time and reduce code duplication. | GoConvey does not have built-in support for test suite setup and teardown. | | Output Formatting | Testify provides colorful and readable output formatting that’s useful for identifying and fixing test failures. | GoConvey’s output is less visually appealing and can be harder to read. |

Testify provides many benefits beyond the standard Go testing package, including a wide range of assertion functions, built-in mocking, test suite setup and teardown, subtests, and better output formatting. While GoConvey also provides some of these features, Testify has a more extensive list of assertion functions and built-in mocking capabilities, making the package a more powerful testing tool.

ConclusionYou’ve learned the importance of testing and the limitations of using the standard Go testing package, explored Testify’s assertion functions, mock testing functionalities, and test suites, learned about TDD and BDD and how Testify supports the testing methodologies, and compared Testify and GoConvey packages and highlighted the advantages of using Testify.

By using Testify in your Go projects, you can write tests that are simple and expressive, thereby ensuring that your code meets requirements with high quality.

The post Step up Your Go App Testing Game With the Testify Framework appeared first on Semaphore.

View Details

Fermyon Spin has become one of the best WebAssembly frameworks. Spin makes the development of your microservice applications faster, more portable, and even more scalable. Spin supports languages like Rust, TypeScript, Go, C/C++, C#, Zig, Ruby, Python, Grain, and even JavaScript. In this article, we will be focusing on Spin JavaScript SDK, which was introduced in December 2022. With Spin JavaScript SDK, you can now build microservice applications.

In this post, we will dig into building a real-world application with the new Spin JS SDK. We will move from installing Spin and its plugins to building a Spin application. It’s pretty easy, but we will delve into the process in this article. We will also discuss how we can make use of the Some-Random-API service to help fetch some random anime quotes.

PrerequisitesHere are some things you should know before reading this article:

  • Basic knowledge of how to use JavaScript.
  • npm >= 8.5.5 or higher installed on your local development machine.
  • Basic knowledge of how to use APIs.

Getting StartedYou need to have everything listed above before diving into things. As we said earlier, we will be using Spin JavaScript SDK to build a random anime quote generator. Firstly, let’s get started by installing Spin on MacOS. We will install the latest version (v1.0.0). This is the first step because the application we’re about to create will also run on Spin. You can download Spin using this link. Alternatively, you can install Spin on your CLI by following these instructions:

  • Run the command below on your terminal to install Spin on your machine:

$ wget https://github.com/fermyon/spin/releases/download/v1.0.0/spin-v1.0.0-macos-aarch64.tar.gz # for Silicon $ tar xfv spin-v1.0.0.tar.gz$ ./spin --help After installing Spin, make sure to add it to your machine’s main path so that you can access it from any directory, as shown below

sudo mv ./spin /usr/local/bin/spin By doing this, you do not have to install Spin in every single directory you want to work on.

Working with Spin JavaScript SDKNow that we have installed Spin and made it accessible in every directory, we can get started with building with Spin JavaScript SDK, but before doing that, note that Spin JavaScript SDK is experimental and that it even supports TypeScript–if you use TypeScript, this SDK is for you too. Working with Spin JS SDK means you’re just working with JavaScript and other Spin components. Now, let’s start by installing the js2wasm plugin. We will now have to run the following command to install the plugin:

$ spin plugin update$ spin plugin install js2wasm The first command spin plugin update helps update the default Spin plugin. For the second command, it calls out Spin to install the js2wasm plugin. The js2wasm plugin aids in the conversion of JavaScript code into WebAssembly and its execution in the browser.

What we’ll do next is install the Spin JS template, and you have to do that right in your Spin CLI with the following command:

$ spin templates install --git https://github.com/fermyon/spin-js-sdkCopying remote template sourceInstalling template http-ts...Installing template http-js...Installed 2 template(s)+-------------------------------------------------+| Name Description |+=================================================+| http-js HTTP request handler using Javascript || http-ts HTTP request handler using Typescript |+-------------------------------------------------+ The command above also installs the JavaScript and TypeScript template from its source, so in this case, you can choose to use TypeScript or JavaScript to build your Spin application, whichever way works! Now that you have installed the template, the next thing for you to do is utilize it.

For JavaScript, here’s how to create a project from the template you installed earlier on:

$ spin new http-js my-project --accept-defaults If you’re done doing this, you should run the cd my-project command to locate the directory you created for the project. After doing this, you can run the code . command to open the project in Visual Studio Code. And if you’re not using VSCode, just open your IDE and locate the project path manually.

Alternatively, if you’re using TypeScript, here’s how you can create your project:

$ spin new http-ts my-project --accept-defaults You can also follow the same instructions as for JavaScript if you want to start with TypeScript. This article is focused on JavaScript, so we will continue with JavaScript. Now that you’ve opened the project in Visual Studio Code, you should get this:

Spin JS Project on Visual Studio Code

You can now examine all of the code. First, you can navigate to the spin.toml file, which is the configuration file for Spin, and you can see every necessary piece of information about the project and its settings. You can also check the webpack.config.js file; the configuration file for the JavaScript code. What we will be working with is the index.js file in the src/ directory; this is where you will write all of your JavaScript code.

You can now check the src/index.js file, and you’ll find the following code:

const encoder = new TextEncoder()export async function handleRequest(request) { return { status: 200, headers: { "foo": "bar" }, body: encoder.encode("Hello from JS-SDK").buffer }} Now that you can see this code, you’ll see that its output should just be “Hello from JS-SDK.” But first, let’s use the terminal to install all of the dependencies by running the following command:

$ npm install Next, you should wait a few seconds until the dependencies are installed before running the following command:

$ npm run build After doing this, you could be asking yourself a very big question: “Why am I installing npm in a Spin project?” It’s essential, as we need it to run your JavaScript code. You’ll also notice that it’s included in the package.json and package.lock.json files.

Finally, to run your application, run the following command:

$ spin upServing http://127.0.0.1:3000Available Routes:my-project: http://127.0.0.1:3000 (wildcard) This should be the output of your code in the browser after running the application.

Hello from JS-SDK

Now, as you can see, the code in the src/index.js file prints out this text in your browser.

Developing the Random Anime Quotes GeneratorFunnily enough, this will be done in the same src/index.js file but using a different concept. Now, we will build a JavaScript application that fetches random anime quotes from the Some Random API platform.

First, get into your spin.toml file. In the [[component]] configuration, you should have the following code:

[[component]]id = "sample-gen"source = "target/spin-http-js.wasm"exclude_files = ["**/node_modules"]allowed_http_hosts = ["https://some-random-api.ml"]files = ["src/index.html"][component.trigger]route = "/..."[component.build]command = "npm run build" The only changes you’ll notice is that allowed_http_hosts = ["https://some-random-api.ml"] and files = ["src/index.html"] have been added. The allowed_http_hosts component trigger calls out the API service we will be using, which allows the API in the src/index.js file to start working.

Now, let’s head to the src/index.js file and get started. Go ahead and add the following code:

const encoder = new TextEncoder("utf-8")const decoder = new TextDecoder("utf-8") The TextEncoder helps convert all strings to bytes, so we need to include that first. Its counterpart, TextDecoder, helps to decode the UTF-8 strings. Rewrite the handleRequest() as follows:

export async function handleRequest(request) { const Quote = await fetch("https://some-random-api.ml/animu/quote") const QuoteBody = decoder.decode(await Quote.arrayBuffer() || new Uint8Array()) const body = `Here's a Quote: ${QuoteBody}\n`} We just used the async function in the first line of code and added the handleRequest function; this serves as an entry point for the Spin component in this case. In the next line, we are trying to call out the API’s endpoint. Meanwhile, in the other lines, we’re trying to structure the response after fetching the API.

Now, add this code before the closing bracket:

return { status: 200, headers: { "foo": "bar" }, body: encoder.encode(body).buffer } This code is just to print the response of the API you fetch, so the whole code in the src/index.js file should appear as shown below:

const encoder = new TextEncoder("utf-8")const decoder = new TextDecoder("utf-8")export async function handleRequest(request) { const Quote = await fetch("https://some-random-api.ml/animu/quote") const QuoteBody = decoder.decode(await Quote.arrayBuffer() || new Uint8Array()) const body = `Here's a Quote: ${QuoteBody}\n` return { status: 200, headers: { "foo": "bar" }, body: encoder.encode(body).buffer }}

Now, you can run the spin build command, which also executes the npm run build command. After doing that, run the spin up command, and you should have random anime facts in your browser just like this:

![A result of the random anime quote generator] (https://cdn.hashnode.com/res/hashnode/image/upload/v1676105458188/371d3166-e78d-421b-a74b-c7280bd87c2f.png align=”center”)

Perfect! Now, you have your Spin application running, and we’re finished.

ConclusionI hope that you had fun learning how to build a Spin application with Spin JavaScript SDK. Even though this tutorial only covers building a random anime quote generator with Spin JavaScript SDK, there’s a lot more you can do with Spin. For example, you can build a WASM todo list app, a simple spin application for serving doom-wasm, or you could even build a game just like Finicky Whiskers. Thanks for stopping by to read this article. You can also check out this sample code on GitHub.

If you have any questions, you can leave them in the comments section below.

The post Streamlining WebAssembly Development With Spin JavaScript SDK appeared first on Semaphore.

View Details

ChatGPT has been on the rise since its introduction. It completely changed how everyone, including developers, uses AI. This chatbot has significantly improved numerous areas, such as precise search results, enhanced human skills, and most importantly, coding.

Due to ChatGPT’s widespread use around the world, it can become overloaded, resulting in lengthy wait times before it’s available for use. Also, it has trouble generating long-form, structured code snippets. Its knowledge is also limited to 2021.

To help with this, we have compiled a list of the top ChatGPT alternatives that are best suited for coding and the needs of developers. Each alternative is discussed alongside its benefits, features, limitations, and pros and cons.

1. GPT-4This is the latest AI model from OpenAI, the developers behind ChatGPT. GPT-4 is an upgrade to GPT-3.5, which are both chat-based LLMs built in ChatGPT. GPT-4 outperforms GPT-3.5 in producing more human-like responses, recognizing content in images, and even completing more challenging tasks accurately.

The GPT-4 model is trained on a larger amount of data than GPT-3.5 and supports a variety of popular programming languages. Also, you can use an extension like GitHub Copilot to access GPT-4 within an IDE.

We discovered that GPT-4 is quite intelligent and fast when used for programming tasks; it finds solutions to problems in under two minutes that would take an average developer several minutes to figure out and perhaps hours to write the code. GPT-4 can turn a rough design sketch into a simple, functioning website, debug code quickly, and analyze code snippets for security vulnerabilities.

The only restriction at the moment is that using GPT-4 requires a $20 monthly subscription to ChatGPT Plus, so it is not free to use.

| Pros | Cons | | --- | --- | | Available when ChatGPT is down | It is not free | | superior to ChatGPT’s free version | | Supports over 15 native languages |

2. GitHub Copilot and Copilot XThis is an “AI Pair Programmer” developed by GitHub and powered by OpenAI technology. It is an excellent AI tool for developers because it is a generative trained model that was built using trillions of lines of code from GitHub public repositories.

Using machine learning, it auto-completes your code based on context, such as comments, variable names, and the code structure. GitHub Copilot works as an extension in code editors with support for various programming languages like JavaScript, TypeScript, Python, and Go.

GitHub recently introduced a new version of Copilot called Copilot X, which is powered using the new GPT-4 model. It provides new features like AI chat assistance, pull request support, test generation, and even inbuilt CLI functionality, all of which work directly from an IDE.

However, GitHub Copilot’s code suggestions are not always accurate, as it occasionally suggests long-form codes when a clearer, shorter one exists, or even deprecated codes.

GitHub Copilot offers a 30-day free trial to test out the AI, after which you pay a subscription fee. It is free to use, however, for students, teachers, and people who maintain well-known open-source projects.

| Pros | Cons | | --- | --- | | Developed by GitHub | It is not free for everyone | | Accessible through Code Editors | Doesn’t support a wide range of languages |

3. OpenAI PlaygroundOpenAI Playground is more of a demo version of ChatGPT but packed with lots of features that can be modified with ease. Its neural network is larger than ChatGPT’s, making it a more advanced AI tool for developers.

A variety of adjustment options are available in the OpenAI Playground, including setting the model type, frequency penalty, token count, loading presets, etc. As a developer, if you’d like to work with the OpenAI API, then the Playground is an ideal place to test out the AI features before integrating it into your app or website.

Given that it uses the same OpenAI technology as ChatGPT, it is just as accurate and effective at answering and resolving coding-related questions.

The one core limitation it has compared to ChatGPT is that it is not free. When you first access the OpenAI playground, you’re given a free trial credit of $18 that you use when prompting the AI. The free credit expires when you use it all up or after 3 months. After that, you won’t have access to the playground unless you purchase additional credits.

| Pros | Cons | | --- | --- | | Extra tweaking options | It is not entirely free | | API available | Only available in English |

4. Bing AIMicrosoft finally found a powerful strategy for challenging Google’s search engine. Bing is now more advanced than traditional search engines, thanks to Microsoft’s integration of AI capabilities trained by OpenAI.

What makes Bing AI stand out in this list is its ability to provide valid, up-to-date code results and cite sources for additional reading.

Additionally, it produces any additional prompts you might require automatically when you ask questions. For instance, when we asked for a JavaScript function that makes use of the map array method, it asked whether we would prefer to use any other array methods to address the issue.

Additionally, it runs on GPT-4 and supports image generation on the fly, and the ability to create dummy images for use in projects can be helpful for developers.

The limitation is that there is a cap on how many prompts you can give the AI in one session. In order to use the AI, you may often find yourself establishing multiple sessions.

Bing AI is currently available for free, and like Google, it’s likely that Microsoft will keep providing users with this AI service for free.

| Pros | Cons | | --- | --- | | Created by Microsoft | Limited prompts per session | | Free to use | | Runs on GPT-4 |

5. Amazon Codewhisperer Amazon CodeWhisperer is similar to how GitHub Copilot works in that it uses AI to suggest code snippets as you type. CodeWhisperer is, at the time of writing, in an unreleased state.

CodeWhisperer is an AI tool that has been trained using a variety of open-source code and code found in Amazon libraries. As a result, it functions incredibly well for developers who work with Amazon services. Although this doesn’t limit its capabilities to only Amazon, it has been tested and performs remarkably well with some programming languages like JavaScript, Python, and Java.

Like GitHub Copilot, you’ll need to access CodeWhisperer through IDEs like JetBrains, VSCode, etc. in order to use it. Once you’ve connected it to your IDE, it will examine the context of your code and suggest potential code that you might want to type. Without leaving your code editor, it’s an efficient way to deal with potential bugs and improve performance issues.

Currently, CodeWhisperer is free since it is still in the preview stage. However, there’s no information on whether it will remain free after its official release.

| Pros | Cons | | --- | --- | | Free to use | Supports few programming languages | | Accessible through code editors | | Created by Amazon |

6. ChatSonicThis is a large-scale, trained model for dialogue response generation that works similarly to ChatGPT. The platform behind this AI chatbot has dubbed ChatSonic the “ChatGPT Alternative Built With Superpowers”.

ChatSonic supports a variety of exceptional features, such as up-to-date code results, voice text-to-speech, and even image generation.

We used ChatSonic to run a number of coding tests, and it did incredibly well. It provided straightforward explanations for many code-related issues and fixed bugs. The limit input for querying the chatbot is 4,000 words per input.

But, in contrast to ChatGPT, we found a few limitations during our testing. For example, the UI is not as good and simplistic as ChatGPT’s, and it is crowded with too much information, giving little space to see the AI outputs. Also, the response speed isn’t as fast as ChatGPT and, although it remembers conversations, it is not effective after about 3–4 inputs.

You can try it out for free before buying the paid version, to see how it works. There are 10,000 free words included in the trial, after which you must upgrade to the paid version.

| Pros | Cons | | --- | --- | | Up-to-date code results | Requires a subscription | | API available | Clustered interface |

7. TabnineTabnine was among the first to introduce auto-suggestion of code snippets in an IDE, similar to how GitHub Copilot works.

Because Tabnine AI has been trained on so many open source programs, it can make code suggestions based on the context of your project. As a plus, developers can train and customize their own AI models within Tabnine.

The intriguing thing about Tabnine AI is that it can run not only on IDEs but also on your computer, in the cloud, on a server, and even offline. This alone makes Tabnine stand out from other AI tools.

Tabnine works with many IDEs, including VSCode, Sublime, Vim, Android Studio, etc., making it one of the most adaptable generative AI tools on this list. Developers actively coding in a variety of popular programming languages, including JavaScript, Python, TypeScript etc would find this tool useful.

Despite its capabilities, Tabnine AI, unlike ChatGPT, is still unable to recommend precise code snippets. It has not been trained on as much data as ChatGPT, which may be the cause of this.

Both free and paid plans are available from Tabnine; $20 a month is the cost of the paid plan, which grants access to more features.

| Pros | Cons | | --- | --- | | Accessible through code editors | Code suggestions may be incorrect | | Runs on more environments | | Free version available |

8. RixRix is an AI chatbot designed specifically for developers and a great alternative to ChatGPT. Hashnode, a blogging platform for developers, built Rix.

Hashnode continually trains the AI model with the most recent data on well-known open-source projects. It provides significant programming solutions with citations, making it a useful tool for developers looking for quick fixes to coding issues.

The AI-generated code can be directly executed on the platform thanks to a useful feature. By enabling developers to test their code without leaving the AI chatbot, this feature may help save time.

It supports a wide range of programming languages and frameworks, including Python, NextJS, TailwindCSS, React Native, and many more. Hashnode offers specialized training for each programming language.

The apparent downside is that it is not as powerful as other alternatives on this list, and it sometimes fails to adequately explain some programming questions.

| Pros | Cons | | --- | --- | | Free to use | Not as accurate as ChatGPT | | Can run code within the chatbot |

9. AskcodiThis AI tool is also fueled by OpenAI, trained using publicly-accessible source code, and built for developers of all levels. This allows it to have a deep understanding of various programming languages.

AskCodi has the distinctive ability to be completely integrated with almost every facet of software development. It has specific sections for frontend use, backend use, code documentation, explanation, testing, and much more.

This chatbot is really well made for developers, as it supports a wide range of programming languages and frameworks, from the popular ones like JavaScript to the lesser-known ones like LUA.

The limitation of AskCodi is that it provides only 100 free tokens for each prompt each month. If you should exhaust all of these tokens before the end of the month, you have to upgrade to the paid tier or wait until the following month.

| Pros | Cons | | --- | --- | | Accessible through code editors | It is not entirely free | | Supports native languages | Slow response speed |

10. Google BardGoogle has also rolled out its own AI chatbot called Bard. However, it hasn’t yet been made available for use worldwide by the public at the time of writing. Only those in the USA and the UK are eligible to try it out.

But here are a few things we do know: Bard is built on a modern, research-based large language model (LLM), which is an organized and upgraded version of LaMDA. The model is based on Google’s in-depth knowledge of reliable information sources.

Because Bard is still in its early stages compared to other alternatives like Bing, it is less efficient at analyzing code for developers. Some code can be accurately explained, but not all.

For more, see Google Bard FAQ,

Bard can’t help you with coding just yet. Bard is still learning to code, and responses about code aren’t officially supported for now.

Given Google’s capabilities, it won’t be long before this AI chatbot ranks among the best for programmers and general users.

| Pros | Cons | | --- | --- | | Created by Google | Still in its early stages |

ConclusionWhile ChatGPT is undoubtedly a powerful AI tool, it’s always a good idea to explore other options and find the one that best fits your needs and preferences. If used well, these alternatives can perform countless time consuming activities like explaining programming concepts, writing complex code, finding bugs in code, giving best practices in software development, and so on.

To sum up, developers should take advantage of the latest advancements in AI (by using one of these options) to enhance their work and achieve better results.

The post 10 Best Alternatives To ChatGPT: Developer Edition appeared first on Semaphore.

View Details

Technical writing presents its own unique challenges (and rewards) as an author. It can be unusually difficult for us, as engineers, as we often spend our days communicating with computers through transcribing arcane symbols and stitching together abstract APIs. Sharing our experiences and learnings from these “odd” types of conversations with other human beings isn’t something that comes naturally and that’s OK. It’s a skill that you have to build, like any other.

I believe it’s incredibly important to put yourself out there, share your knowledge, become vulnerable and open to feedback. This post is written for those of us interested in helping others but aren’t sure where to start.

The section titles below map to P.S. as a mnemonic to help you remember them!

Problem StatementThis is an easy, but often overlooked point: clearly state the problem you are trying to (or have successfully) solve(d). If you haven’t figured out how to do it yet, but already learned some invaluable lessons, it’s still worth sharing.

This is your hook for the reader, so be sure to give some background as to why you thought that this problem was interesting enough to tackle. Maybe you didn’t find any other explanations/solutions on the Internet and, having invested considerable time and effort, just want to give back to the community. Or perhaps the other approaches you did find were too convoluted, misguided or downright incorrect and you don’t want others to have to struggle like you did.

Taking the time to explain your motivation gives the reader their first inklings of understanding your thoughts and opinions.

Present SolutionOleksandr V. Savytskyi, PhD Dissertation Defense, 30 May 2017 (cc)You’ve probably spent untold hours poring over insufficiently documented and unexplained intricacies of the problem at hand. With this knowledge and experience, it’s hard to rewind backwards to that blissful period of ignorance and naivete, but good stories take time to develop.

Be sure and start at the beginning (the problem statement) and the steps you took, both forward and back, towards the end goal. It may seem convoluted or a waste of time to describe in detail the wrong turns and mistakes you made, but these really do help encourage others to:

  • Not make the same mistakes
  • Improve the product/service so others don’t run into the same road blocks
  • Acknowledge your humanity (and spark a connection)

Even trivial solutions should be shared: you might have accidentally solved it in an incredibly elegant way that will baffle others. But, most importantly, the very act of explaining your solution by writing it down in a post helps crystallize the learning. Taking the time to organize and present your findings for others helps you truly “finish” and add that bright green checkmark.

Publish SourcesIf at all possible, share your solutions in the form of open source code. From Gitlab to Github, there are all kinds of online source code repositories that champion the hosting and distribution of software applications and infrastructure automations.

Give people access to the nitty gritty cogs, wheels, buttons and levers of your solutions. Let them fork your codebase to improve upon or embark on wildly new, better ideas. It’s a big step putting yourself “out there” and sharing your code with the rest of the world, but it does get easier the more you do it. Remember, taking this step means you’ve been hyper-vigilant with your secrets.

Peer ScreeningSarahmirk, 22 Apr 2017 (cc)Focused work on a complex issue sometimes gives us superpowers. We get into a state of “flow” where things don’t need explanation and we feel a “god like” understanding of the topic. Stepping back from these heady heights to impart your wisdom to “mere mortals” is extremely challenging.

Hand over your (most likely) incoherent scrawlings to someone far removed from the topic and get their honest feedback on what you’ve written and how well you’ve conveyed your message. We all inwardly flinch at the term “feedback”, but embracing it can really make the difference between unfathomable tripe and convincing.

Take the time, swallow your pride and rework your text. Having the chance to solicit others’ opinions on your writings is invaluable. Taking the opportunity to clarify your intent before publishing is worth its weight in gold.

Please StartYou miss 100% of the shots you don’t take. The fact that you invested time & effort in pursuing a solution to this problem, means there’s a non-zero chance someone else has/will have the same problem and is/will be looking for a solution. Too often, we finish coding/scripting/building and move onto the next problem without adequately publishing our findings.

It’s lonely out there! I’ve written hundreds of posts since 2007 and very few have gotten meaningful feedback or engagement. Almost a decade ago, I needed to learn how to set up millions of rewrites in nginx. A quick Google search turned up absolutely nothing. Months later, after I figured it out, I wrote the blog post “Supporting Millions of Rewrites with Nginx in Lua and Redis”. To this day, folks still reach out to thank me and that means all the world.

Imagine how difficult our road would be if all the giants, on whose shoulders we stand today, hadn’t published and explained their work to us. Open communication and more transparency on the things that motivate us will only clear the path for those that follow in our footsteps.

Share the love by passing on what you have learned!

Find out more at our Technical Writing Meetup – May 4, 16pm CET.
Register here for the free talk.

The post Technical Writing for Humans (P.S. I Love You) appeared first on Semaphore.

View Details

How companies move during changes can determine their success, but also nurture eventual failure. In this episode, we will explore how Software Consultant Susanne Kaiser uses Wardley mapping and Domain-Driven Design to identify the core domain of businesses and apply the appropriate method per evolution stage, how to improve team organization, and how evolution changes teams.

Edited transcriptionSusanne Kaiser’s diverse experience in the software development industry —she has worked as a software developer, software engineer, team lead, and startup CTO— has equipped her with a comprehensive understanding of the software development lifecycle. Besides, it allows her to take on diverse responsibilities and roles as a consultant; given her coding background, she refers to herself as a “coding software architect.”

Wardley mapping to determine business strategyWardley mapping is an invaluable asset in Susanne’s consulting toolbox. Using Wardley maps we can visualize, from left to right, the evolution of the components of a business over time, from the unstable beginning to more stable, standardized components, typically categorized into four stages: genesis, custom-built, product, and commodity: Genesis components are novel and new, custom-built are specifically created for a particular use case, product components are standardized and widely available, and commodity components are ubiquitous and often provided as a utility.

By analyzing their own Wardley map, organizations can identify which components are —and are not­— critical to their business. Wardley maps help organizations to focus on innovation in the areas that matter the most given the value they provide and let them know where to use standardized and commoditized components where possible to reduce costs. Note: For more information about Wardley mapping, check our conversation with Simon Wardley, its creator.

Even though Susanne initially struggled to figure out how Wardley mapping could be useful in her work, she later realized that she could create Wardley maps of the value chain from users, user needs, the components that fulfill those needs, and the evolution stages of these components.

Sussane learned to combine Wardley mapping with Domain-Driven Design (DDD) to outline the many subdomains within organizations and find their core domain and its evolution. Her work as a consultant starts by identifying the pain points and bottlenecks of the organization and its teams, the pace at which they change, and their value chain using Wardley mapping —or team topologies in some cases.

As they are drawn, Wardley maps become a landscape of users and their needs that help her “understand the domain and challenge assumptions within the team.” Sussane uses this map as a foundation for future architecture, that is, how it should look and what is the ideal landscape the company would like to deal with, in which, for example, they could have gotten rid of the less efficient components in the value chain.

Once Sussane has identified the components, she “brings in the lens of Domain-Driven Design” to challenge the value chain. Domain-Driven Design (DDD) allows her “to decompose some of these components into smaller modular components, the bounded context, and also how to ensure high cohesion and loose coupling between those bounded contexts with context patterns.

Once the domains have been identified, Sussane reviews how companies look upon their core domain. Since the core domain is “the business critical part of your system that shall provide a competitive advantage,” she encourages clients to increase their efforts in developing it. In like manner, at this stage, she expands her Wardley map “to figure out whether there are custom-building commodities that are not quartered to the business” and learn if her clients are “investing a lot of effort into something where they’re not differentiating themselves.

Wardley mapping recognizes that the core domain can also evolve, and when this occurs, “the differentiation advantage of all competitive advantages increases or decreases” Sussane explains. As such, after specifying the core domain and its evolution stages, the next step is implementing the appropriate methods per evolution stage: “Build components in Genesis and custom built in-house with preferably agile methods or use off-the-shelf products, a bio use of the shelf products, or open source software solutions for components in product and rental with preferably lean methods or outsource to utility suppliers for components in commodity and utility or preferably Six Sigma methods,” Sussane recommends.

Team organization and evolution through Wardley maps and DDDIn Wardley maps, the components on the left side, those closer to the Genesis stage, demand more exploration and are characterized by uncertainty. They require more cognitive load than components on the right side, which are more stable and well-known. On this ground, Sussane suggests allocating more bounded context ownership to teams on the left side of the map.

In DDD, a bounded context is a well-defined boundary within which a particular domain model applies. It is an area of the system where the domain concepts, entities, and rules are clearly defined and understood. Sussane suggests identifying team boundaries that correspond to bounded contexts in the value chain, which are areas of the system where a specific team is responsible for a particular aspect or function. By allocating more bounded context ownership to teams who have to deal with exploration and discovery, we can ensure that these teams have the autonomy and resources they need to explore and experiment with the domain using new ideas. Simultaneously, the teams at the right side of the spectrum can focus on their specific area of expertise and reduce cognitive load by not having to switch between areas constantly and instead focus on routine tasks or maintenance activities.

In terms of team composition, Sussane reminds us of one of the doctrinal principles of Wardley mapping: thinking of aptitude and attitude as a way of distributing power and decision-making: “We have in our organization not only aptitude skills that are relevant for team consolidation but also attitude,” she says. From this perspective, aside from their technical skills, teams should also have a personal preference for their specific area of expertise. A team dealing with constant changes and unknown components needs a different attitude than a team dealing with established components and best practices. More importantly, each will perform tasks according to their ability. In Susanne’s view, we should consider which components a team is responsible for when building or bringing teams together so they can perform at their best. In this same regard, depending on which stage the product is, its ownership might be passed to another team.

However, Susanne also points out that teams should have a t-shaped skill model not completely oriented toward a skill or attitude, but a tendency that will not exclude having different skills aside from their personal preference. The idea is to have teams collaborate to help each other in the way they know best. She emphasizes that although teams may be stable, the organization itself is not and changes over time, and so do the interaction modes between teams depending on the context. Similarly, given their different skill sets, we should expect teams to interact and collaborate in different ways over time.

At the same time, knowledge sharing between areas as part of the company’s culture will align teams and help them adapt to change faster. In turn, a lack of communication, says Sussane, equals “introducing bottlenecks where others have to wait and get frustrated to get their changes delivered to production.” In like manner, Susanne mentions the importance of having architectural principles in place (that is, documented and accessible for everyone) to ensure that the organization’s architecture is aligned with the principles and characteristics.

Lastly, Sussane points out that as companies grow from a handful of engineers to hundreds and thousands, leaders will be demanded new skills and are expected to take decisions that they might not be fit to take: “The same people can rarely handle the same context at the same scale,” says Sussane, who encourages changing leadership altogether as the company grows. Former leaders can remain in the organization, fulfilling other roles while handing over the leadership responsibility to a new generation.

The bottom lineSussane Kaiser is working on her book Adaptive Systems for a Fast Flow of Change which will hopefully be published this year. She can be followed on Twitter, Mastodon, and LinkedIn. She has also given talks at tech conferences available on Youtube about the concepts she shared with us and others. Sussane is open to discussing any challenges that her readers or followers may be facing and is always willing to answer questions. Keep an eye out for her book and don’t hesitate to reach out to her for support.

The post Susanne Kaiser on Wardley Mapping appeared first on Semaphore.

View Details

On April 28th, 2023, Semaphore experienced an incident that impacted its cloud CI/CD services. The incident started at 22:23 UTC and lasted until 01:43 UTC, during which time the triggering of the jobs on Semaphore was severely delayed, leading to a partial loss of service for many users.

Our on-call SRE team was alerted by automated monitoring systems and initiated an investigation into the issue.

The root cause was determined to be low-performing database queries that, under specific conditions, caused the production database CPU usage to spike to 100%. The problem was resolved by implementing a solution and cleaning up the jobs queue.

Timeline (all times in UTC) 22:23: Incident start * 22:34: On-call SRE team alerted by automated monitoring systems * 23:05: Problem identified as database performance issue; issue escalated to additional engineers * 00:29: Root cause found: low-performing queries causing production database CPU usage to spike to 100% under specific conditions * 01:05: Solution implemented, resolving the issue * 01:24: Job processing queue cleaned up and job processing resumed, returning service to operational state * 01:43*: Incident end

Root Cause AnalysisThe database performance decrease was primarily due to the degraded performance of the DB table responsible for managing job states. Although this table is relatively small, it experiences a high read/write rate, leading to the rapid accumulation of dead tuples.

The auto-vacuuming function, which is responsible for cleaning up these tuples, was not executing as expected due to increased traffic in other parts of the DB cluster. This inaction resulted in further database performance degradation and increased load.

Resolution and RecoveryUpon identifying the issue, our engineers paused the subsystem responsible for job scheduling and performed a manual vacuum operation on the jobs database table. This operation was completed at 01:05 UTC, at which point the service loss was mitigated, and the system returned to normal.

An additional 19 minutes were required for the job queue, which had built up since the start of the incident, to clear. By 01:24 UTC, the system was fully operational again.

Corrective Actions and Preventative MeasuresTo prevent similar incidents in the future, we will:

  1. Conduct a comprehensive review of our database queries and performance metrics to identify any other potential bottlenecks or areas for improvement.
  2. Implement regular performance testing and monitoring to proactively identify and address performance issues before they escalate to incidents.
  3. Enhance our automated monitoring systems to better detect and alert on potential performance-related incidents.

We apologize for the inconvenience this incident caused our customers and are committed to continuously improving our service reliability and performance.

Incident Response Policy BreachDuring the investigation of this incident, we also identified an internal breach of our incident response policy. Our policy mandates that any service disruption impacting multiple users must be communicated through our public status page. Unfortunately, this communication protocol was not followed in this instance.

To prevent this oversight from happening again, we will:

  1. Re-evaluate and update our internal incident response policy to ensure it is comprehensive, clear, and up-to-date with our current processes.
  2. Conduct additional training sessions with our engineering and support teams to reinforce the importance of adhering to the incident response policy and effectively communicating with our customers during incidents.
  3. Ensure that the SRE team always has a dedicated support team member on standby during incidents. This will guarantee that any progress made in resolving the issue is promptly and accurately communicated to the impacted customers, maintaining transparency and keeping them informed throughout the process.

We recognize that timely and accurate communication is crucial during service disruptions, and we are committed to improving our processes to ensure transparency and accountability.

The post Service Outage Postmortem: April 28 appeared first on Semaphore.

View Details

As software programs grow in complexity, so does the possibility of bugs that disrupt the program’s operations. Manually testing applications is not feasible or reliable and can lead to unreliable software that is prone to crashes or unexpected behavior.

Unit testing is a software testing method that involves testing individual units or components of a software application in isolation from the rest of the application. It aims to validate that each code unit is working as intended and to catch any bugs or errors early in the development process before they become more challenging to fix.

In this article, you will learn how to write and run high-quality unit tests in Node.js using the popular testing framework Jest.

Setting up Jest in Node.jsBefore you can start writing unit tests with Jest, you have to install it as a dependency in your project and create test files.

Run the command below to install Jest:

npm install --save-dev jest After you have successfully installed Jest, run the command below to create a folder for your Jest test files:

mkdir test The folder created by the command above will store all your test files.

Next, you will create a test file. Test files are files you write your tests in. Jest considers files with the .test.js and .spec.js extensions to be test files.

You can create a test file by running the command below:

touch example.test.js Finally, add the command below to your scripts in your package.json file:

"scripts": { "test": "jest" } The script above allows you to run all your test files with a single command.

Creating tests with JestRecall that Jest considers files ending in .test.js to be test files. Thus, all your tests should follow this naming convention.

For example, consider the function below:

//index.jsfunction isEven(number) { if (number < 0) throw new Error("Number must be positive"); if (typeof number !== "number") throw new Error("Number must be a number"); return number % 2 === 0;}module.exports = isEven; The function above takes a number as an argument and returns true if the number is even and false if it is not.

To test this function, import it into your test file and create a test suite for the function.

A test suite is a collection of test cases grouped together to test a specific functionality or feature. In this case, your test suite will test the functionality of the isEven function.

The things you’d need to test in the isEven function include if:

  • It returns true if the number is even.
  • It returns false if the number is odd.
  • It throws an error if the number is negative.
  • It throws an error if the argument is not a number.

Here’s an example of a test suite created with Jest that tests all the possible scenarios for the isEven function:

//example.test.jsconst isEven = require("../index");describe("isEven", () => { test("returns true if number is even", () => { expect(isEven(2)).toBe(true); }); test("returns false if number is odd", () => { expect(isEven(3)).toBe(false); }); test("throws an error if number is negative", () => { expect(() => isEven(-1)).toThrow(); }); test("throws an error if number is not a number", () => { expect(() => isEven("1")).toThrow(); });}); In Jest, the describe function is used to group related test cases. In this case, the test cases are related to the isEven function, so we use "isEven" as the description. The function takes a callback housing all the individual tests related to the feature you’re testing. In this case, the individual tests for the isEven function.

The test function defines a single test case, similar to the describe function but for a single case. In the first two test cases, the test confirms if the function returns the appropriate boolean by passing an even and odd number, respectively. It confirms the return values using the Jest matcher toBe.

In the last two test cases, the test confirms if the function throws an error when called with an invalid argument (negative number/not a number) using the Jest matcher toThrow.

You can run the test suite by executing the command below:

npm test Running the command should produce the output in the image below, as all tests should pass.

Testing Asynchronous CodeNot all JavaScript code runs synchronously; some runs asynchronously to avoid blocking code execution because it takes time to complete.

By default, Jest’s tests complete once they reach the end of their execution. That means that when you test asynchronous code, you need to let Jest know when the code it is testing has finished executing before it can move on to another test. Else the test will not work as intended.

Take the code block below as an example:

// fetchdata.jsfunction fetchData() { return new Promise((resolve, reject) => { setTimeout(() => { resolve(JSON.stringify({ name: "Jon Snow", age: 30, email: "jonsnow@housestark.com", })); }, 1000); });} This fetchData function in the code block above is asynchronous. It doesn’t return a value directly but instead takes a callback function as a parameter, which it calls with the data after a delay of 1 second.

Here’s a test for the fetchData function that doesn’t account for its asynchronous nature:

// fetchdata.test.jstest("fetchData returns the correct data", () => { const data = fetchData(); expect(data).toEqual( JSON.stringify({ name: "Jon Snow", age: 30, email: "jonsnow@housestark.com", }) );}); Although the test looks like it should pass, if you run the test above, it will fail, as shown in the image below.

The test failed because Jest completed its testing before the fetchData function could return its value.

Jest has several ways to test asynchronous code, including using Promises, async/await, callbacks, and Jest’s resolves/rejects matcher.

Using Jest’s resolves matcher, you can rewrite the test like so:

test("fetchData returns the correct data", () => { return expect(fetchData()).resolves.toBe( JSON.stringify({ name: "Jon Snow", age: 30, email: "jonsnow@housestark.com", }) ); }); The resolves matcher extracts the value of a fulfilled promise so you can chain any other matcher to make your assertions. With this matcher in place, the test should pass.

Mocking Dependencies with JestMocking dependencies is a common practice in testing; by replacing a real dependency with a fake one that you can control and manipulate during the test. This can be useful for testing code that relies on external resources or services, such as APIs or databases.

Jest provides built-in mocking capabilities that make creating and using mock functions and modules easy.

For example, suppose you have a module called user.js that depends on a module called database.js for database operations.

Here’s how you can mock the database.js module to test the user.js module without actually accessing the database:

// user.test.jsconst user = require("./user");const database = require("./database");jest.mock("./database");describe("getUser", () => { test("should return the user with the given userId", () => { const userId = "123"; const userObj = { id: userId, name: "John" }; database.getUser.mockReturnValue(userObj); const result = user.getUser(userId); expect(result).toBe(userObj); });}); Here, we’re mocking the database.js module using Jest’s built-in jest.mock() function. This function replaces the actual module with a mocked version, which you can configure to return specific values or behave in a certain way.

In the getUser test case, we’re using the mockReturnValue method of the mocked database.js module to specify that when the getUser function is called with a certain userId, it should return a specific userObj. Then, we’re calling the getUser function of the user.js module with the same userId and expecting it to return the same userObj.

By mocking the database.js module, you can isolate and test the getUser function of the user.js module without actually accessing the database.

Comparing TDD vs. BDD patterns on JestTest-Driven Development (TDD) is a software development methodology that involves writing tests before writing the code.

The process begins by writing a failing test case, which tests a small unit of code. Then you write the minimum amount of code necessary to pass the test case. Once the test case passes, you can refactor the code as required and repeat the process for the next test case.

On the other hand, Behavior-Driven Development (BDD) is a software development methodology that focuses on describing the behavior of the software in plain language that is understandable to all team members.

In BDD, tests are written as “scenarios” that describe the expected behavior of the software from the user’s perspective. BDD tests are often written using a natural language syntax that makes them more readable and understandable by non-technical team members.

Jest supports both TDD and BDD patterns, and you can choose to use either approach based on your preference and the needs of your project.

TDD with JestLet’s walk through a scenario that explains TDD in Jest using a function that returns the product of two numbers.

Let’s start with writing out the test for a function that returns the product of two numbers.

// multiply.test.jsconst multiply = require("./multiply");describe('multiply function', () => { it('should return the product of two numbers', () => { expect(multiply(2, 3)).toEqual(6); expect(multiply(0, 5)).toEqual(0); expect(multiply(-2, 3)).toEqual(-6); expect(multiply(-2, -3)).toEqual(6); });}); Next, run the test and see it fail. Running the test above would fail because multiply doesn’t exist yet.

Then, implement the function from the results obtained from your failed tests, like so:

function multiply(a, b) { return a * b;} Rerun the test and see if it passes. Now that you’ve written the multiply function, the tests should pass.

The final step in TDD is to refactor the code if necessary. In this scenario, we implemented a multiply function that doesn’t need refactoring.

BDD with JestLet’s walk through a scenario that explains BDD in Jest using a function that returns the product of two numbers.

The first step is to describe the function’s behavior in a way that both technical and non-technical people can understand. You can achieve this using the Gherkin syntax.

After you have described the function’s behavior, you’ll create a Jest test file and implement the tests for this scenario following the described behavior, like so:

// multiply_bdd.test.jsdescribe('Multiplication', () => { test('Multiply two numbers', () => { // Given const a = 5; const b = 10; // When const result = multiply(a, b); // Then expect(result).toEqual(50); });});function multiply(a, b) { return a * b;} Following the BDD approach ensures that your tests are closely aligned with the desired behavior of your code.

Jest vs. MochaJest might be a very easy-to-use and powerful testing library, but is it the right testing framework for your project? Other testing frameworks like Mocha and Puppeteer, among others, are alternatives with unique features that make them stand out.

Jest and Mocha are both popular JavaScript testing frameworks you can use for writing unit tests. Here’s a table highlighting some of the differences between the two.

| Jest | Mocha | | Configuration | Jest is an opinionated framework. This means that it comes with preconfigured settings and testing utilities out-of-the-box. | Mocha requires a lot of configuration and setup before you can use it for testing. | | Assertion Library | Jest comes with its own assertion library. | Mocha doesn’t have a native assertion library. You have to use it with dedicated assertion libraries, such as Chai. | | Mocks | Jest has a built-in mocking library. | Mocha does not have a built-in library for mocks. It depends on other packages, such as Sinon, for mocks. | | Performance | Jest is generally known for having faster test execution times than Mocha. Jest achieves this by combining parallelization, intelligent test file selection, and caching. | While it may be slower than Jest out-of-the-box, Mocha can be optimized for faster test execution times using plugins and configurations. |

Ultimately, the choice between Jest and Mocha depends on your needs and preferences. Jest might be the better choice if you want a framework with a lower learning curve and more out-of-the-box functionality. If you prefer a more customizable framework and want to work with your own assertion and mocking libraries, Mocha might be the better choice.

ConclusionIn this article, you learned how to set up Jest in Node.js, create and run test suites, and mock dependencies for your tests using the Jest testing framework. Additionally, you explored BDD and TDD testing patterns and how to implement them in Jest. Finally, to give you a wide array of testing frameworks to choose from, we compared Jest to Mocha, another popular testing framework in the Node.js ecosystem.

You can learn more about Jest in the official Jest documentation.

Learn more about testing:

  • A Deep Dive into Unit Testing
  • The Testing Pyramid: How to Structure Your Test Suite
  • The 6 Principles of Test Automation

The post Writing Unit Tests in Node.js Using Jest appeared first on Semaphore.

View Details

Today we are introducing a feature called Deployment Targets (available in our Startup and Scaleup plans).

As we all know, deployment to production is a procedure that should be approached with caution. This is especially the case in highly regulated environments. Customers are required to have proper, bullet-proof deployment control mechanisms that will allow them to stay compliant with all the industry regulations.

In this case, having a tight control over who can run a promotion and push code to production is a must. With the help of deployment targets, you can limit who can trigger promotions to specific environments, save secrets directly in deployment targets and set other crucial conditions for deployment to start.

Let’s take a closer look at what the feature entails.

What are deployment targets?Deployment targets allow your organization to specify which pipelines exactly can deploy to this or that target, identify who can trigger a deployment, and limit from which branches, tags, or pull requests your team can deploy.

You can find deployment targets under Project > Deployments. All deployment targets have an under-area that displays the status of the latest deployment for that target:

  • 🟢 Green indicates that the deployment was successful
  • 🔴 Red means that the deployment failed
  • 🔵 Blue means that the deployment is in progress

For each deployment target, you can see the following information: the name of the target (1), its status which tells us if the deployment target is active or temporarily disabled (2), who has the ability to deploy and where from (3), who deployed last time and when (4), the deployment branch and pull request (4).

On the dashboard, you can also choose to edit or delete the deployment target.

Deployment target historyTo access deployment target history, click the View full history button on the dashboard.

Here, you can see all the events associated with this specific deployment target: when the latest deployment happened and who triggered it. With this you can have a clear overview of all your deployments and you can also use filters (by branches, tags, pull requests or users) to browse through all the deployments that were executed.

How to create a new deployment target?To create a new deployment target, hit the button “Create new”. The setup wizard will take you through the steps needed to setup a new target.

Step 1. Specify name, description and URLOn this step, you’ll need to provide some information that will help your team to identify what the target is and what it does. For example if this deployment target is used to build a mobile version of our app (that is named Friendly Traffic Light) to the App Store, we can name it friendly-traffic-light-ios-production-build. Remember to add as much context as possible to keep things clear.

Step 2. Specify credentialsOn this step, you’ll need to provide a set of variables that will help your pipeline to connect to this target. You can enter the secret keys and specify paths to files that you’ll need to connect to the deployment target. Think of this as secrets, but secrets that are only stored in this deployment target and not viewable on a project or organization level.

Step 3. Define who can deployOn the Scaleup plan you can select the users that will be able to deploy the target. You can also choose specific roles (e.g., deployers or admins). In this case, all the people with these roles assigned will be able to deploy.

You can also select specific people and add them to the list. The rules for deployers sum up. So, everyone who meets any of the conditions (has a certain role or is specifically selected as a deployer) will be able to deploy.

Step 4. Limit branches, tags and pull requestsHere, you can limit from which branches, pull requests and on which tags your team can deploy to this specific deployment target.

Step 5. Adding deployment target to your promotionNow add the deployment target we have created to our builds by going to the Workflow Editor, selecting your promotion and specifying the deployment target.

Your deployment target is now ready!

Deployment targets demoSubscribe to our Youtube channel to view the demo once it is available!

How are deployment targets helpful?Deployment targets empower your team to add an extra layer of security for your deployments. With the help of deployment targets, promotion will be triggered only if all the conditions are met. Define who exactly can trigger the promotion, on what branches and tags.

Here’s a list of all the limitations and conditions that you can set for your deployment targets:

  • Limit who can run promotions
  • Limit which pipelines can use which secrets
  • Limit who can rerun a pipeline and indirectly cause an auto-promotion
  • Limit who can run a pipeline over the API

Are you interested in learning more about the feature? Feel free to reach out to us at support@semaphoreci.com, and we’ll be happy to help.

Happy building!

The post Deployment Targets – Limiting Access to Promotions appeared first on Semaphore.

View Details

You need Docker images to build and ship Docker containers. An image is the base of your container. Thus, keeping images slim and light speeds up the build and deployment of containers. Optimizing Docker images should be practiced in your containerization workflow. Your Docker image size matters for reasons such as:

  • Faster deployments time to download, transfer and load into the container runtime, and improved team productivity and application performance.
  • Better storage utilization on your machine.
  • Reduced network bandwidth when transferring between hosts and container orchestration environments.
  • Reducing image size and removing unnecessary files eliminate vulnerable components that expose images to security issues.
  • Build and portability efficiency speeds up the build processes and improves resource usage.

This post discusses best practices and strategies for slimming and reducing Docker image size.

Best strategies to slim Docker imagesBelow are strategies you can use to help create slim Docker images.

Multistage BuildsWhen building a Docker image, you write instructions using a Dockerfile. Assuming you are running a Node.js application, below you can see an initial example of a Dockerfile that packages and builds the image:

This is a pretty simple example. The Dockerfile commands are straightforward to follow. It implements a single-stage build scenario in Docker.

```

Node.js base imageFROM node:19# Docker working directoryWORKDIR /app# dependencies copy commandCOPY package.json ./COPY package-lock.json ./# Install dependencies on DockerRUN npm install# Copy application filesCOPY . .# Application start scriptsEXPOSE 4000CMD npm start

``` This script will be enough to get a simple hello world Node.js application running on Docker. However, it creates a rather large image, even for the smallest codebase. You should expect this size to increase as the application size grows.

Things get more complex if you have multiple environments — you end up maintaining many Dockerfiles and many images. It can get expensive in the long run to maintain and rebuild development and production images.

Multistage builds allow you to slim Docker images. This allows you to define multiple stages in your Dockerfile to represent the different stages of the build process, using multiple FROM commands. Each represents a different stage of the Docker image build process. In this case, every stage you create has a different base image that executes different commands to build and package your application.

The final image of your application is created by copying code files and dependencies from the previous stages. This means Docker will discard any intermediate files and build artifacts that are no longer needed to create your final build.

```

Stage One: Development ApplicationFROM node:19-alpine AS baseWORKDIR /appCOPY package*.json ./RUN npm installCOPY ./CMD npm start#Stage Two: Create the Production imageFROM base AS finalRUN npm install --productionCOPY ./CMD npm start

`` This approach is well articulated when you have an extensive application with development dependencies that you don’t want to run in a production environment. However, you can still use the multistage build to target a development application image. A-targetflag specifies thebase` stage. This way, you can still run your development Docker image using the following command as an example:

docker build --target base -t image_example . This will only execute the base phase, allowing you to isolate different stages in your Dockerfile.

The benefits of multistage builds are:

  • You end up with a slimmer image.
  • You don’t have to create different Dockerfiles for development and production purposes.
  • You only have to maintain one file. The Multistage build approach combines the Dockerfile environments and creates a single artifact ready for production builds.

Choosing base imagesA Docker base image creates the foundation for structuring your Docker images. They provide pre-built images with the tools and libraries required to run your applications in containers. Docker provides different variants of such base images optimized for specific use cases.

While using the above Node.js example, the image runs on node:16 as the base image. Node.js has various optimized image versions. Each image has tags for the respective Dockerfile.

The above base image is set to node:<version>, where the version is the Node.js version you want to use. This versioning instructs Docker to pull your base image with all package Node.js artifacts. As a result, you will end up creating images with large disk sizes.

However, you can opt to use other versioning variant alternatives as distribution base images, such as:

  • node:<version>-alpine – This variant uses Linux Alpine distributions to ensure that your base image is 5 MB in size. Here is an example of an alpine image:

```

You Node.js base imageFROM node:19-alpine3.16# Docker working directoryWORKDIR /app# dependencies copy commandCOPY package.json ./COPY package-lock.json ./# Install dependencies on DockerRUN npm install# Copy application filesCOPY . .# Application start scriptsEXPOSE 4000CMD npm start

`` *node:-slim` – Node.js provides this variant to only the common packages needed to run Node.js, thus providing even smaller base images. The slim variant will consequently reduce Docker image size as follows:

```

You Node.js base imageFROM node:19-slim# Docker working directoryWORKDIR /app# dependencies copy commandCOPY package.json ./COPY package-lock.json ./# Install dependencies on DockerRUN npm install# Copy application filesCOPY . .# Application start scriptsEXPOSE 4000CMD npm start

``` The choice of the base image predicts the size outcome of your images. Always check the tags and variants your base image provides to reduce base image size. Make sure to check, however, that the variants you use include the right dependencies for your application.

Docker image layersA Docker image is sectioned into layers. Layers are created based on how you write your Dockerfile: every Dockerfile command or line creates an image layer of its own. These layers are the Docker image file system.

Take this as an example:

FROM node:16WORKDIR /appCOPY package.json ./COPY package-lock.json ./RUN npm installCOPY . .EXPOSE 4000RUN npm start Every Dockerfile command, e.g. FROM, WORKDIR, COPY, RUN, COPY, EXPOSE, and RUN, creates an image layer of its own. The layers are then used to assemble the final image. The FROM command creates the first layer, followed by the other commands in series.

Over time, as you add more commands, the image will accumulate layers and increase in size. You should always prune image layers to produce slim, optimized Docker images. This will compress Docker commands.

Here, there two COPY commands that can be slimmed down as follows:

FROM node:16WORKDIR /appCOPY package*.json ./RUN npm installCOPY . .EXPOSE 4000RUN npm start Adding a wildcard tells Docker to copy both package.json and package-lock.json under one layer.

Below is another example that shows you how to combine RUN commands. This image clones the code from GitHub; thus, it requires Git to be installed in the container.

FROM node:19-alpineRUN apk add --no-cache gitRUN mkdir -p /appRUN chown node:node /appWORKDIR /appUSER nodeRUN git clone https://github.com/Rose-stack/node_app.git .RUN npm installEXPOSE 4000CMD npm start It can be effectively slimmed as follows:

FROM node:19-alpineRUN apk add --no-cache git && \ mkdir -p /app && \ chown node:node /appWORKDIR /appUSER nodeRUN git clone https://github.com/Rose-stack/node_app.git . && \ npm installEXPOSE 4000CMD npm start While these are very simple examples, I hope you understand the concept of reducing the image layers by combining commands that have similar operations.

Combining this approach with a multistage build and an optimized base image will significantly reduce Docker image size. We can, however, slim images even further!

Add .dockerignoreNot every file and folder needs to be copied to the Docker container. Using commands such as COPY . . instructs Docker to copy all files and folders from your local directory to the Docker container.

For example, when creating a Node.js image, you have folders and files such as node_modules, .dist, build, and npm-debug.log. Docker does not need them, and the install and build commands will create them. Thus, copying them over to Docker will increase your image size. To avoid that, in your Dockerfile root directory, always create a .dockerignore file so Docker can ignore them. Furthermore, .dockerignore can also be used to avoid copying files with sensitive data, such as passwords, into the image.

Leverage compression toolsCombining all the above practices, is guaranteed to help reduce Docker image size, but we can do even more. To further reduce size, you can leverage compression tools and slim Docker images. These tools include:

  • Dive
  • Docker Slim
  • DockerSquash

Below is a simple example of how to use Docker Slim to reduce Docker image size. First, ensure you have Docker Slim installed.

To reduce the size of your image, Docker Slim uses the build command. The build command functions as a Docker Slim compression method to minimize Docker image size. Let’s see how this works using a typical Docker image example. The following command pulls an Nginx command:

docker pull nginx:latest Check its original size with the following command:

docker images nginx:latest Use the following Docker Slim command to compress your image:

docker-slim build --sensor-ipc-mode proxy --sensor-ipc-endpoint 172.17.0.1 --http-probe=false nginx; To confirm the results, check your image size again:

docker images This shows that Docker Slim reduced the image from 135MB to around 12.4MB. This illustrates how Docker Slim can trim and compress your image by roughly 90.37% of its initial size.

ConclusionSlimming and reducing the size of your Docker images improves your containers’ overall builds and deployments. Using the practices and strategies discussed in this post, you can create slim and trim Docker images that will provide faster build times and lower storage and transfer costs. You can use all the above strategies in every image to achieve the optimal size.

The post The Best Strategies to Slim Docker Images: How to Reduce Docker Image Size appeared first on Semaphore.

View Details

Stimulus is a JavaScript framework that’s designed to augment your HTML with modest ambitions and just enough behavior to make it shine. It doesn’t seek to take over your entire front-end, nor is it concerned with rendering HTML.

In this exercise, we’ll take a closer look at the key features of Stimulus JS and examine the appropriate use cases for each one. We’ll explore how Stimulus JS can be used to add interactivity and functionality to server-side rendered applications, and we’ll consider the benefits of its lightweight architecture and modular approach to building user interfaces.

PrerequisitesYou will need the following. I am using a MacOS.

  • Docker installed
  • Knowledge of Ruby on Rails
  • Knowledge of Javascript

Finished GameThe codebase of the completed game can be found HERE.

NOTE: If you’re encountering any obstacles in getting things to function correctly, please refer to the repository where the completed game codebase is available.

Setting up a new rails appCreate a folder on your machine and navigate into it. I will use the name matching-game-demo

mkdir matching-game-demo && cd matching-game-demo Generate Dockerfile with the following content. This file is used to build our docker image.

FROM ruby:3.1.0 WORKDIR /matching-game RUN apt-get update -yqq \ && apt-get install -yqq --no-install-recommends \ build-essential \ curl \ gnupg2 \ libpq-dev \ nodejs \ npm \ && npm install -g npm \ && npm install -g yarn \ && rm -rf /var/lib/apt/lists/* RUN curl -fsSL https://deb.nodesource.com/setup_19.x | bash - \ && apt-get update -yqq \ && apt-get install -yqq --no-install-recommends nodejs \ && npm install -g esbuild EXPOSE 3000 Generate docker-compose.yml file with the following content. Docker compose helps us spin up everything or tear it down using a single command.

services: web: build: context: . dockerfile: Dockerfile tty: true volumes: - .:/matching-game ports: - "3000:3000" command: bin/dev In your terminal, run the command to access bash inside our Docker container.

docker compose run web bash Inside bash, we can confirm that npm, yarn, bundler and node are present by checking their versions.

Install the rails version you want to use.

gem install rails -v 7.0 Generate a new rails app. The dot (.) means to create a new app in the current folder and use that as the name. In our case, ‘matching-game-demo’

rails new . --css=tailwind --javascript=esbuild Exit the bash session.

Edit the Dockerfile and append commands for copying the generated files to the container and running system setup, i.e. dependencies, installations, and database migrations.

COPY . /matching-game/ RUN yarn install --check-files RUN bin/setup In terminal, run the following command:

docker compose build To start the web server, run the command and visit http://0.0.0.0:3000 in your browser.

docker compose up Game board user interfaceIn another terminal, navigate to the matching-game-demo/ directory and generate the dashboard controller with the index method.

docker compose run web rails generate controller dashboard index Set the dashboard index method as the root path.

```

./config/routes.rbroot "dashboard#index"

`` The game board is a two-dimensional square. In this article, we will only focus on the dimensions of2,4and6`.

Create a variable called @board_size.

```

./app/controllers/dashboard controllerdef index # Note: This can be either 2,4 or 6 @board_size = 4end

`` Next, we will create the tiles. The total number of tiles for the board isboard_size ^ 2`.

A tile will be a div element which has the index value as its content.

```

<% (@board_size * @board_size).times do |index| %>
<%= index %>
<% end %>

``` Right now, the structure of the board is wrong; we want a square shape. We can solve this by using tailwind grid.

```

<% boardClassName = " bg-gradient-to-r from-violet-500 to-indigo-500 p-8 rounded aspect-square justify-center items-center grid gap-6 grid-cols-#{@board_size} grid-rows-#{@board_size} " tileClassName = " shadow-sm hover:shadow-2xl rounded bg-white justify-self-center w-24 max-w-36 aspect-square "%>

...
...

``` Before we test in the browser, let’s keep a few things in mind:

  • We are setting the background color of each tile to White using bg-white n tileClassName
  • The board’s grid, rows, and columns count are set dynamically using grid-cols-[VALUE] and grid-rows-[VALUE] where the VALUE is @board_size.

Upon testing, you may have noticed that when you change the @board_size value on the dashboard controller, the board structure does not persist.

This is because Tailwind does not work well with dynamically-set classes. To avoid this issue, we can preload the expected classes before we use them.

This is done using tailwind safelist.

Source: tailwindcss-rails repo on Github.

```

tailwind.config.jsmodule.exports = { content: [...], safelist: [ 'grid-cols-2', 'grid-rows-2', 'grid-cols-4', 'grid-rows-4', 'grid-cols-6', 'grid-rows-6', ],}

``` Last, let’s add styling to the HTML body element.

```

... <%= yield %>

`` **Stimulus game board controller**Create a stimulus controller and attach it to theboard div` element.

docker compose run web rails generate stimulus board

```

...

... ``` We can add a `console.log` to the connect method of the board controller for testing. ``` /* ./app/javascript/controllers/board_controller.js */...connect() { /* For testing. Remove when done. */ console.log("Board controller connected")} ``` When we reload our UI and inspect the console, You can see the test “Board controller connected”. The `connect` method is called when the page with the HTML element `data-controller` is loaded. Before we proceed, go ahead and change the following: * Replace `bg-white` with `bg-black` in the tileClassName. * Remove the index text content inside each tile div element. Next, we want to add a `click` action to each tile, which changes the background color of the clicked tile to red and sets the text content to its index value for 1 second and then reverts these changes. Refer to Stimulus Actions to learn more. ``` ...
``` ``` /* ./app/javascript/controllers/board_controller.js */...flip(event) { const tile = event.target const tileIndex = Number(tile.dataset.tileIndex) this.#showContent(tile, tileIndex) setTimeout(() => { this.#hideContent(tile) }, 1000)}#showContent(tile, tileIndex) { tile.textContent = tileIndex tile.style.backgroundColor = "red" tile.classList.remove("bg-black")}#hideContent(tile) { tile.textContent = null tile.style.backgroundColor = null tile.classList.add("bg-black")} ``` When a tile is clicked, the `flip` method is called and we extract both the tile – the div element – and tileIndex. Then we call the private method `showContent`, which sets the text content to the `tileIndex` value, the background color to `red`, and also it removes the tailwind class `bg-black`. One second later, the `hideContent` reverts the showContent changes. **NOTE:** We access `data-attributes` on the stimulus controller using camelCase. For example: ``` const tileIndex = Number(tile.dataset.tileIndex) ``` **Game board store outlet**Our game needs to have the following features: * When an opened tile is clicked again, nothing happens. * Compare if two successively clicked tiles have the same background color and text content. * Do nothing if the two successively clicked tiles have the same background color and text content * Reset the two successively clicked tiles if their text content and background color are not the same. This is a lot of functionality to have inside the `board` controller file. Stimulus provides a solution to this called Outlets that allows us to reference other stimulus controllers from within our board controller. To solve the double clicking of the same tile, let’s add `data-tile-is-open=false` to our tile div element and if that value is `true`, we do nothing. ``` ...
... ``` ``` /* ./app/javascript/controllers/board_controller.js */...flip(event) { const tile = event.target const tileIndex = Number(tile.dataset.tileIndex) const tileIsOpen = String(tile.dataset.tileIsOpen) if (tileIsOpen === "false") { ... }}#showContent(tile, tileIndex) { ... tile.dataset.tileIsOpen = "true"}#hideContent(tile) { ... tile.dataset.tileIsOpen = "false"} ``` Now we can create the store stimulus controller that will act as an outlet. ``` docker compose run web rails generate stimulus store ``` ``` ...
...
``` **Note: You can either use ID or CLASS to pass the element whose controller you want to be the outlet. I am using ID.** ``` /* ./app/javascript/controllers/board_controller.js */export default class Controller { /* Name should match the controller name */ static outlets = [ "store" ] ... flip() { /* For testing only. Remove when done */ this.storeOutlet.test() ... } ...} ``` ``` /* ./app/javascript/controllers/store_controller.js */export default class Controller { ... /* For testing only. Remove when done */ test() { console.log("A tile has been clicked") }} ``` When we click a tile, we can see the text “A tile has been clicked” in the browser. Next, we use the store controller to `store` an array of successively clicked tiles. These are the features we want: * Clicking a single tile more than once only pushes that tile to our array once. * Reset tiles when the array size is exactly two. * Only reset the tiles in the array. ``` /* ./app/javascript/controllers/store_controller.js */export default class Controller { connect() { this.successiveTilesCollection = [] } addToSuccessiveTilesCollection(tile){ this.successiveTilesCollection.push(tile) } resetSuccessiveTilesCollection() { this.successiveTilesCollection = [] } get successiveTilesCollectionCount() { return this.successiveTilesCollection.length }} ``` ``` /* ./app/javascript/controllers/board_controller.js */export default class Controller { ... this.#showContent(tile, tileIndex) this.storeOutlet.addToSuccessiveTilesCollection(tile) if (this.storeOutlet.successiveTilesCollectionCount === 2) { setTimeout(() => { this.storeOutlet.successiveTilesCollection.forEach(tile => { this.#hideContent(tile) }) this.storeOutlet.resetSuccessiveTilesCollection() }, 1000) }} ``` You will notice that now you have to click 2 different tiles before both of them are reset. **Game board tiles content**For a game board that has correct matching tiles, we need `(@board_size ^ 2 / 2)` unique items, i.e. tiles, that have the same background color and text content. For example, for a 4 by 4 board, we need 18 unique tiles. Let’s create two collections for color and label. ``` # ./app/controllers/dashboard_controller.rbclass DashboardController < ApplicationController ANIMALS = ['dog', 'cat', 'pig', 'owl', 'ant'] # Add more COLORS = ['red', 'green', 'yellow', 'lime', 'pink'] # Add more def index @board_size = 4 @board_finished_result = generate_board_result(@board_size) # will build this next endend ``` Before we can build the `generate_board_result` feature, this is what we want: * We want to express a tile’s contents as `[color, label]`, meaning that our board will be an array of arrays. We will employ `array#product` from ruby’s documentation. Examples of `array#product`: ``` # ruby irb> [1,2].product([3,4])(result) [ [1,3], [1,4], [2,3], [2,4] ]> ['cat', 'dog'].product(['pink'])(result) [ ['cat', 'pink'], ['dog', 'pink'] ] ``` Now, time to build the `generate_board_result`. ``` # ./app/controllers/dashboard_controller.rb...privatedef generate_board_result(size) board = create_board(size)enddef create_board(size) return [] unless size.even? color_options = COLORS.shuffle.take(size / 2) letter_options = ANIMALS.shuffle.take(size) options = color_options.product(letter_options) (options * 2).shuffleend ``` As you can see, have the board tile values generated and randomized in the array. Next, we hashify the board so that the hash key joins `color` and `label`, and the hash value will be an array of the indexes of the tiles that have those contents. Example of the expected hash result is: ``` { 'red--dog': [0,2], 'green--monkey': [1,3] }(explanation)For a 2x2 board, tiles whose label is "dog" and background color is "red" are at indexes positions 0 and 2 ``` ``` # ./app/controllers/dashboard_controller.rb...def generate_board_result(size) board = create_board(size) hashify_board(board).to_jsonend...def hashify_board(board) result = {} board .group_by.with_index { |_, index| index } .transform_values { |value| value.join("--")} .each do |key, value| result[value] ||= [] result[value] << key result[value].uniq! end resultend ``` We can pass the `@board_finished_result` to our board controller via Stimulus Values, which allow us to read and write HTML data attributes in controller elements as typed values. ```
... ``` ``` /* ./app/javascript/controllers/board_controller.js */export default class extends Controller {static outlets = [ "store" ]static values = { finishedResult: { type: Object, default: {} }}connect() { /* For testing. Remove when done. */ console.table(this.finishedResultValue) ...} ``` When we test it in the browser, we can see the hash contents in the browser console. Let’s ensure that when we click a tile, the correct contents of that tile from `finishedResultValue` is what is set instead of the current hardcoded values. ``` /* ./app/javascript/controllers/board_controller.js */flip(event) { ... if (tileIsOpen === "false") { const currentTileContent = this.#extractTileContentsFromFinishedResult( tileIndex, this.finishedResultValue ) this.#showContent(tile, currentTileContent) this.storeOutlet.addToSuccessiveTilesCollection(tile) ... ...}#showContent(tile, data) { tile.dataset.tileIsOpen = "true" tile.textContent = data.label tile.style.backgroundColor = data.color tile.classList.remove('bg-black')}...#extractTileContentsFromFinishedResult(tileIndex, finishedResultValue) { const tileKeyInBoard = Object .keys(finishedResultValue) .filter(key => { return finishedResultValue[key].includes(tileIndex) }) if (tileKeyInBoard.length < 1) { return {color: null, label: null, indexes: [] } } const result = tileKeyInBoard[0].split("--") return { color: result[0], label: result[1], indexes: finishedResultValue[tileKeyInBoard[0]] }} ``` Now, each tile has the correct content based on the `finishedResultValue`. Let’s handle not resetting successive tiles collection if the last clicked tile has the same contents as the tile being clicked. ``` /* ./app/javascript/controllers/store_controller.js */...connect(){ ... /* matchingIndex is the index of the tile with same contents */ this.lastClickedTile = { matchingIndex: null }}...updatePreviouslyClickedTile(currentTileIndex, indexes) { this.lastClickedTile = { matchingIndex: (indexes.filter(i => i != currentTileIndex)[0]) }}resetPreviousTile() { this.lastClickedTile = { matchingIndex: null}}get tile() { return this.lastClickedTile} ``` ``` /* ./app/javascript/controllers/board_controller.js */ ... flip(event) { const tile = event.target const tileIndex = Number(tile.dataset.tileIndex) const tileIsOpen = String(tile.dataset.tileIsOpen) if (tileIsOpen === "false") { const currentTileContent = this.#extractTileContentsFromFinishedResult( tileIndex, this.finishedResultValue ) this.#showContent(tile, currentTileContent) this.storeOutlet.addToSuccessiveTilesCollection(tile) if (this.storeOutlet.successiveTilesCollectionCount === 2) { setTimeout(() => { if (this.storeOutlet.tile.matchingIndex !== tileIndex) { this.storeOutlet.successiveTilesCollection.forEach(tile => { this.#hideContent(tile) }); } this.storeOutlet.resetSuccessiveTilesCollection() this.storeOutlet.resetPreviousTile() }, 1000) } else { this.storeOutlet.updatePreviouslyClickedTile( tileIndex, currentTileContent['indexes'] ) } } } ``` When you test, you will notice that when you open two matching tiles successively, the tiles are not reset. And that concludes our game. To sum up, Stimulus JS is a powerful Javascript framework that provides a simple and efficient way to create interactive and dynamic user interfaces. Its core features, including controllers, actions, targets, values and outlets allow developers to add behavior and functionality to specific parts of a page in a modular and maintainable way. **What next?**You can try adding the following features: * Improve the `create_board` method on Dashboard controller. * Add a timer counter. * Add a move counter. * Add a difficulty select tag level ie `@board_size`. * Add a congratulations message when the board is solved. * Replace the tile label with an image of the actual animal. * Use typescript. * Code cleanup and refactoring. * Try an 8 by 8 board. The post Build a Memory Game with Rails, Stimulus JS, and Tailwind CSS appeared first on Semaphore.

View Details

Docker helps developers make containers for their software. Containers make it easier to use and share said software. Docker logging is an essential aspect of operating containers in a production environment. It provides the capability to track and troubleshoot your applications. Utilizing it can help you identify and fix issues.

To make logging work well in Docker containers, you need to log the app, the host computer, and the Docker service. You can efficiently log your Dockerized apps by combining multiple logging methods and strategies. In this article, I’ll talk about logging in Docker and give tips to help you manage logs in your Docker setup well.

What is Docker Logging?Docker logging refers to collecting and storing log messages generated by containers and Docker components. Log messages are an essential tool for troubleshooting various issues. They are essential for understanding the behaviour of containers and detecting performance problems. They help in debugging applications and understanding the system’s behaviour.

Docker supports several ways to log messages. These include logging to files, logging to the standard output stream, and logging to third-party log management tools.

Challenges with Docker LoggingWhile logging is a critical aspect of Docker container management, it also presents several challenges, including:

  • Volume and Variety of Logs: Docker containers create much log data. This data grows fast as more containers are added. Each container makes various logs, each with its own format and detail level. This variety of logs can make it challenging to add and manage log data in a centralized manner.
  • Container Mobility: Docker containers are portable, and you can move them between different hosts and environments. This mobility can make it challenging to ensure that logs are consistently collected, transported, and stored.
  • Log Storage and Management: storing and managing log data in a scalable and reliable manner can be challenging, especially when there’s a lot of log data. Solutions that work well for small-scale environments may become overwhelming or challenging to manage as the scale increases.
  • Container Ephemerality: containers in Docker can be created and destroyed dynamically, which can make it challenging to ensure that logs are consistently captured, stored, and retained. Logs may be lost when a container is deleted, and log data may not persist if a container is recreated.
  • Logging Configuration: configuring logging in Docker can be complex, particularly for teams that are new to Docker or for large-scale environments. Issues such as log rotation, storage, and transport need careful consideration and planning.
  • Debugging and Troubleshooting: debugging and troubleshooting issues in a Docker environment can be challenging due to the sheer volume of log data generated by containers. Logs can be dispersed across multiple containers, hosts, and storage solutions, making it difficult to locate relevant logs for a particular issue quickly.
  • Compatibility with Different Log Formats: different applications, containers, and systems may generate logs in different formats and use different logging tools. This can make it challenging to consolidate and analyze log data in a unified manner.
  • Security and Compliance: in many cases, logs contain sensitive information and are subject to regulations, such as GDPR, HIPAA, and PCI DSS. Ensuring the security and compliance of log data in a Docker environment can be challenging, particularly when it comes to protecting logs from unauthorized access and ensuring that logs are retained for the required period.

Now that we are familiar with some of the challenges in docker logging, let’s move on to discuss some of the best practices. For better understanding, I’ll be grouping them into two subsections so as to grasp different aspects for docker logging and also general techniques for logging.

How to Properly LogIn this section, the focus is on how to properly configure Docker logging to ensure that log data is captured effectively and efficiently. This includes:

1. Log to the Standard Output StreamThis can be helpful when working with Docker containers that generate simple log output and also where the log data is transient, meaning it doesn’t need to be persisted long-term. By logging to the standard output stream, you can ensure that logs are accessible from the Docker host, making them easier to manage and track. Also, doing so:

  • Allows you to collect logs in a consistent and accessible manner.
  • Ensures that logs are available on the host, which can be useful in the event of container failure.
  • Makes it easier to monitor logs, as they are stored in a standard location.

2. Limit the log sizeDocker containers can generate a large amount of log data, which can cause performance issues if not appropriately managed. To avoid this, experts limit the log size to a reasonable amount, e.g. 10 MB per container.

This practice is necessary as large logs can slow down systems and make log analysis more difficult. Also, limiting the log size will help ensure that logs are accessible from the Docker host, making them easier to manage and track. Limiting log size is important to manage the amount of log data generated by containers. By limiting the size of logs, it becomes easier to manage disk space and prevent logs from consuming too much storage or, in worst case, filling the disk entirely!

3. Rotate LogsRegular log rotation is necessary to avoid logs growing too large and using up disk space. You can do this using log rotation tools like logrotate. Rotating logs helps you archive or delete old ones and retain only the most recent ones. Doing so helps prevent disk space from being exhausted, which can impact system performance and stability. It also helps to ensure that log data is secure, as older logs can be deleted or archived, reducing the risk of data breaches.

Rotating logs can be useful in scenarios where logs are generated frequently, and it’s important to manage the amount of log data generated by containers.

How to Leverage Log DataThis section will focus on how to use log data to gain insights and improve the performance and reliability of your Docker environment.

4. Use a centralized logging solutionThis practice can come in handy when managing multiple Docker containers across different hosts, and it’s important to aggregate log data in a central location. By centralizing log storage, you can have all your logs in one place, making it easier to manage, search, and analyze them. Doing so allows you to have a centralized view of all the logs generated by your Docker containers, which can be helpful in troubleshooting and monitoring. You can use Centralized logging solutions like ELK (Elasticsearch, Logstash, and Kibana) or Graylog to manage logs from multiple containers. These tools provide a centralized view of log data, making identifying and resolving issues easier. Using a centralized logging solution can help in the following ways:

  • It makes log analysis more efficient, as all logs are stored in one location.
  • It helps to ensure data security, as logs are stored in a centralized location, reducing the risk of data breaches.

5. Tag log messagesIt is vital to tag log messages with relevant information such as the container name, hostname, and date and time. Doing so makes it easier to identify the source of log messages and helps to debug. Also, Tagging logs makes monitoring and auditing log data more manageable and secure. Tagging logs can also help in the following ways:

  • It allows you to quickly find relevant logs, making log analysis more efficient.
  • It helps to categorize logs, making it easier to search or even purge log categories that you know you don’t need to retain (GDPR, etc.).

Tagging logs also comes in handy when trying to identify specific log messages related to a particular container or application.

6. Monitor logsRegular monitoring of logs is essential to detect, diagnose, and resolve issues promptly in your application. Doing so allows you to identify and resolve problems before they become more serious quickly. Proper logging can also guarantee the effective operation of your application. In addition, log monitoring can help ensure that your data is secure, as it can help detect security breaches and other security incidents.

There are many automated log monitoring tools available, such as Sentry, Datadog, and Logwatch. These tools can help keep track of your application’s logs and provide valuable insights into its performance. By automating log monitoring, you can ensure proper application operation, and gather and plot important business KPIs, such as the number of sales per hour.

7. Store logs for a sufficient amount of timeStoring logs for an adequate period is vital for maintaining a historical record of application activity. It can come in handy where it’s important to retain log data for compliance, auditing, or troubleshooting purposes. By storing logs for a sufficient amount of time, it becomes easier to analyze historical log data and identify trends and patterns over time. The retention period depends on the requirements of your organization, and it’s prudent to ask your company’s legal team for advice when it comes to log storage.

NOTE: Organizations can keep logs for much longer by cleaning and removing personally identifiable metadata and other information (PII). There are ways of cleaning out PII and extracting meaningful metadata for business KPI monitoring/tracking i.e. data aggregation, data masking, data archiving etc. This data may be stored as long as the business requires it.

Storing logs is necessary as it helps in the following ways:

  • It allows you to review log data over time and identify trends and patterns in application behaviour.
  • It provides valuable data for troubleshooting and auditing purposes, as past log data can be used to resolve issues and understand how problems occurred.
  • It supports compliance with regulatory requirements that may require the retention of log data for a specific period of time.

ConclusionDocker logging is a critical part of container management. It requires careful planning and management. These best practices can mitigate some of the challenges associated with Docker logging.

By following these best practices, you can effectively manage logs in your Docker environment and ensure that your containers run smoothly. Would you please share if you found this helpful?

The post Docker Logging: 7 Best Practices appeared first on Semaphore.

View Details

Secrets are an important part of any CI/CD pipeline as they allow you to store sensitive data such as API keys, passwords, certificates, and SSH keys. In Semaphore, we already offer the ability to store secrets on an organizational level, and today we are offering an extra level of security by introducing project-based secrets. Project-based secrets are limited to the project where they are set up and can not be used outside the project scope.

Best practices for secretsThe principle of least privilege should be followed when establishing access control on an organizational and project level. This means that a user that is present in your project should only have access to the specific data, resources and applications needed to complete a required task. So we want to separate our secrets by the following criteria:

  • Teams that are using them
  • People that need them
  • Projects that need them
  • Reusability

Organization exampleLet’s say our organization is called Friendly Traffic Light and we have two projects in our organization, Green Light 🟢 and Blue Light 🔵. Our team consists of the following fictional members that work on both projects:

  • Cordelia 🧑‍💼 who works on 🟢 🔵
  • Sydney 🧔 who works on 🟢
  • Zainab 🥷 who works on 🔵

The data that we use in our pipelines is:

  • An analytics key that is common for all projects (let us mark this as ⚙️)
  • AWS deployment data (each project has their own AWS account, project green light has 🗝️ and project blue light has 🔑)

By the principle of least privilege, the project green light and project blue light team should only see their own AWS environments, that is why we put that data in the project secrets accordingly, while the analytics environment is common knowledge, that is why we put it in the organization secrets.

In the end we have the following secrets structure (as can be seen by the people in our organization):

| Person | Secrets available to person | | Cordelia | 🧑‍💼 | ⚙️🗝️🔑 (analytics, AWS for green light and AWS for blue light) | | Sydney | 🧔 | ⚙️🗝️ (analytics, AWS for green light) | | Zainab | 🥷 | ⚙️🔑 (analytics, AWS for blue light) |

Setting up in SemaphoreProject secrets are edited the same way as organization secrets. To setup a project secret in Semaphore, just do the following:

Go to your Project and click on Settings:

Click on the Secrets tab and then on the Add new button.

Now you can name your secret and add environment variables or configuration files just like you would be using organization secrets.

Click on Save Secret.

Upon Save you should have the following view with added environment variables:

To use the secrets in your blocks, edit a block and expand the secrets section:

Project Secrets DemoYou can also check the following video on how to setup the organization example we have shown above.

Would you like to know more about secrets?Make sure to check out the docs to learn more about the feature.

Happy building!

The post Introducing Project Secrets appeared first on Semaphore.

View Details

In the age of digitalization, open-source security has become a critical issue for organizations. With more and more companies relying on open-source software to power their applications, ensuring its security is essential. Unfortunately, many companies tend to view open-source components as being more secure than in-house development and fail to take adequate measures to protect their systems from malicious actors.

This post focuses on the importance of open-source security and how it can be managed effectively to safeguard an organization’s data and IT infrastructure. We will explore critical topics such as understanding the threats posed by vulnerable open-source components, identifying potential risks within your system, implementing best practices for secure development processes, and utilizing tools that help you maintain a secure environment. By taking these steps now, businesses can ensure that they remain safe from cyber attacks in the future.

Benefits of Open-SourceOpen-source software has become increasingly popular over the past few years, and with good reason. Open-source solutions offer a variety of benefits that make them an attractive option for organizations looking to get the most out of their technology investments.

  • Collaboration and Innovation: open-source projects have the potential to be more collaborative than traditional software development as they allow users, developers, and companies around the world to come together to create powerful new products. This enables teams to work together on complex projects quickly and efficiently while providing invaluable insight into how others approach similar problems. Additionally, open-source projects often provide users with access to cutting-edge technologies, allowing them to stay on top of industry trends and develop innovative solutions that can set them apart from the competition.
  • Flexibility and Customization: open-source solutions also allow organizations to customize their applications according to their specific needs. With open-source software, organizations are not limited by vendor features or licensing restrictions, meaning they can tailor products to fit their exact requirements without worrying about compatibility issues or prohibitive costs. This makes it easier for businesses to flexibly meet customer demands and cost-effectively.
  • Lower Costs: open-source solutions offer significant cost savings compared to traditional proprietary software. They eliminate upfront license fees and generally require less maintenance and support due to the large community of contributors who help maintain the codebase. As a result, businesses can save money on IT infrastructure costs while still getting access to high-quality software that meets their specific needs.

Risks of Open-Source SoftwareOpen-source software is unfortunately too often given a security “pass” within organizations, which can result in data breaches or system outages if not managed correctly.

Vulnerabilities in CodeJust because a software is “open-source” doesn’t mean there are no vulnerabilities in its source code. Most reliable open-source projects release updates faster than your company can keep up. Keep an eye on their release notes and be on the lookout for security fixes you should incorporate into your application. In addition, many open-source packages are developed for general use, meaning that specific components may be abused/misused by the organization relying upon it. This can lead to potential security loopholes that malicious actors could exploit.

Lack of Security UpdatesBefore importing/relying on open-source libraries, take the time to review their update cycles and disclosure policies. Many companies fail to update their included open-source libraries when new vulnerabilities are discovered or when patches are released. Without timely security updates, organizations may be unaware of potential risks and leave their systems vulnerable to attack.

Dependency on Third-Party CodeOrganizations using open-source code must also be aware of the dependency on third-party code. As more and more businesses rely on open-source libraries and frameworks, they can become dependent on these components for critical applications and services. An organization must ensure that any third-party libraries they are using are up-to-date and secure to reduce the risk of exploitation via malicious or outdated code.

Limited SupportOpen-source software often has limited or no support from its developers and contributors, making it difficult or impossible to receive assistance when issues arise. Furthermore, many open-source components are available as “as-is” packages with no warranties, meaning that organizations must take responsibility for ensuring the security of any products they use.

Difficulty in Monitoring & Managing SecurityMonitoring and managing security is an essential aspect of software development, regardless of whether an organization uses open-source or closed-source software. While some may argue that open-source software is more susceptible to vulnerabilities due to the increased number of contributors and the lack of official support, the reality is that any software can have security issues.

It’s essential to prioritize due diligence in the selection of open-source software and keep the libraries up-to-date to ensure the security of an organization’s systems. Companies must take responsibility for the security of their applications and services, regardless of whether they rely on open-source or closed-source software.

In some cases, organizations may face challenges in monitoring and managing security. Still, these challenges are not unique to open-source software development.

Impact of Security BreachesSecurity breaches can devastate organizations, resulting in financial losses, damage to reputation, and the loss of sensitive information. These risks are particularly pronounced for businesses that rely heavily on poorly maintained open-source components to power their applications and services.

If these systems become compromised due to vulnerabilities or outdated code, it could lead to severe consequences. Financial losses may include costs associated with fixing the breach and potential fines or legal action if data is stolen or leaked.

Reputation damage is also a significant concern as customers may lose trust in an organization that fails to adequately protect its data and IT infrastructure. Also, there is always the risk of sensitive information being exposed through an open-source security breach, which could have severe implications for an organization’s long-term success.

Case Study of NPM PackageNode-IPC is an open-source interprocess communication package that enables developers to quickly and easily create reliable remote connections between programs written in JavaScript.

In March 2022, a security incident involving their software occurred when an author of the Node-IPC package added malicious code to find geolocation and update all files with a heart emoji for all Russian and Belarusian IPs. This was dubbed “Protestware” due to its use for political protests in Russia.

The security incident caused alarm among Node-IPC users, particularly those in Russia who were worried about their data being compromised. It also highlighted the potential security risks that come with blindly relying on open-source software packages, especially when they are developed by third-party contributors rather than the original authors.

The incident served as a stark reminder of why organizations need to be aware of potential vulnerabilities associated with open-source software packages, even when famous authors or organizations develop them.

Best Practices for Open-Source SecurityOrganizations should take certain steps to ensure the security of their integrated open-source software and libraries.

Review Open-Source PackagesFirst, they should review all open-source components they are using and ensure they are kept up-to-date with the latest patches and updates. This will help mitigate the risk of vulnerabilities being exploited by malicious actors. Also, organizations should establish clear policies for selecting and using open-source components to minimize their reliance on third-party code and reduce their risk exposure.

Implementing Security-Focused Development processesOrganizations should also implement security-focused development processes and ensure that all open-source code is adequately tested and reviewed for potential vulnerabilities. Companies should conduct security testing throughout the software lifecycle to identify vulnerabilities early, and during post-release audits to ensure that there are no new risks.

Additionally, companies should provide developers with the necessary education and training on secure coding practices so they can write and maintain secure applications.

Conducting Security Audits and TestingConducting security audits and testing is essential to ensure software security. Organizations should undertake security audits regularly to identify potential vulnerabilities, such as outdated code, insufficient authorization checks, unencrypted data storage, or lack of logging and monitoring.

Security testing should also be performed throughout the software development lifecycle in order to detect and mitigate any vulnerabilities during the design phase. This includes static code analysis tests, which allow developers to analyze their code for any potential flaws without executing it; dynamic tests, which involve executing the code in a test environment; and penetration testing, which simulates attacks on specific parts of the system to determine its vulnerability.

Integrate SCA Solutions in your CI/CD PipelineThe best practice for open-source security is to integrate Software Composition Analysis (SCA) solutions into your CI/CD pipeline. SCA solutions provide automated scanning of codebases for potential vulnerabilities and outdated components, which helps identify issues quickly and efficiently before they become exploited by malicious actors.

Additionally, developers can easily integrate these solutions into existing development processes without disrupting workflow or slowing down release cycles. Integrating SCA solutions into your CI/CD pipeline is an effective way to ensure that your organization’s software remains more secure.

ConclusionOpen-source software packages have been popular among developers and organizations since the 1990s, for their cost-effectiveness and ease of use. However, with the growing digital transformation and adoption of online services by larger, more risk-averse companies, there is a greater emphasis on open-source software’s potential security risks. To ensure the security of their open-source code, organizations should take steps such as reviewing all open-source components and establishing clear policies for their selection and use, implementing security-focused development processes, conducting security audits, and testing and integrating Software Composition Analysis (SCA) solutions into their CI/CD pipeline.

By taking these steps, organizations can reduce the risk of vulnerabilities being exploited by malicious actors, identify any potential issues early on in the software lifecycle, and ensure that developers are trained in secure coding practices. Additionally, integrating security solutions into your CI/CD code commit/build pipeline can help automate scanning for vulnerabilities and outdated components in order to quickly and efficiently detect any potential issues before they become a problem.

Ultimately, open-source security is essential to ensure that organizations can effectively manage their resources while also protecting themselves from potential threats. By taking proactive measures such as those outlined above, organizations can prevent sensitive information from being compromised while continuing to leverage the benefits of open-source software packages that have been popular for decades.

The post The Importance of Open-Source Security appeared first on Semaphore.

View Details

Github has pushed the limits of collaboration in coding as a platform for discussing, sharing, and participating in code and projects. Yet, new tools are looking to push the limit even forward. Zed is a collaborative code editor that offers real-time interaction among teammates inside a code development environment. Its creator, Nathan Sobo, envisions a revolutionary tool that will impact social coding collaboration similar to how GitHub has revolutionized file system snapshots. In this article, we explore the backend development of Zed and Nathan’s insights on creating a performance-optimized system in Rust.

Edited transcriptionNathan Sobo is the creator of the Atom editor and software development framework Electron. His experience creating these two popular apps is proving handy for his current project, Zed, a collaborative code editor “that focuses on performance, clean design, and being multiplayer.” With pair programming in mind, the idea behind Zed’s development is to build a tool that enhances real-time collaboration in code design and makes “it really easy to interact with your teammates directly inside your code authoring environment.”

Having practiced pair programming since the beginning of his career, Nathan understands that in collaborative coding “there’s so much more interaction that potentially needs to happen than just discussing some changes that I want to introduce.” To him, collaboration is narrowed by the ways of working asynchronously. Instead, he proposes to ask about the code even before getting started or getting immediate assistance when encountering an issue, so collaboration takes place “from when you’re thinking about implementing something all the way through implementing it and also when you’re done and you want to introduce those changes.”

As such, despite Nathan isn’t against working alone —and admits he doesn’t pair-program as much as before— he believes that “a little bit of synchronous interaction just really goes a long way and spreading knowledge about the code through the code base, just cross-fertilizing ideas,” has a positive effect in trusting your coworkers and solving issues together.

The backend development of Zed and why performance is criticalNathan asserts that performance is the most important factor to guarantee Zed’s adoption. Hence, while at the beginning they thought of using Rust just for the application backend and using Electron for rendering web content, Zed’s team observed that Electron’s resource footprint was diminishing the performance they were gaining at the core.

The solution was integrating graphics rendering in the core application through shader code (code run by the GPU) written in a metal shader language, which allows this integration. The Zed team even built their own UI toolkit for the app; the OS only renders the window buttons. As Nathan explains, since Zed is completely written in Rust, it can “redraw the entire window on the GPU on every update, just like a video game”, resulting in an “extremely low latency.” At the same time, Zed was integrated with the macOS event loop, so they didn’t have to write their own render loop.

Achieving network collaborationIn Zed, the server and clients operate as a single system that spans the network boundary. Zed users can add other users as contacts by their GitHub usernames and invite them to participate in their projects. After that, a web socket connection is created to the Zed server, and the other user’s remote procedure call (RPC) can see requests through the server to understand the paths in the project. When the other user opens a file, they will also use RPC to send an open request.

Additionally, Zed uses a conflict-free replicated data type to represent data in the buffer. When users edit files, they apply their edits to their local copy and exchange operations to converge on the same content. Zed structures data in such a way that a little bit of latency is acceptable for some actions, like opening a file, but not for editing, which requires real-time exchange of data.

Testing edge casesFor testing in Zed, Nathan applied much of what he had learned in past projects. Specifically, the fact that unit tests will create a solid product, but will only get you so far. As a large number of people begin to use the application, they will inevitably encounter bugs in unlikely scenarios that were not previously tested.

Nathan finds that “the only way to find bugs is just to stress test our data structures and our software” with fuzz testing, in which a random number generator bombards “data structures with every possible interaction path.” Nathan and the rest of the team used this approach to test the program network components and found lots of unforeseeable bugs with it.

The solution to address these bugs was writing a custom executor that allowed them to integrate effectively with the macOS event loop. This custom executor was only used for tests and was designed to run non-deterministic, concurrency-laden integration tests in a repeatedly deterministic way. They seeded the executor with a random number generator and simulated all the network interactions, enqueuing all the places where IO might introduce asynchrony onto the deterministic executor.

In this way, explains Nathan, as he and his teammates encountered 0.1% of cases where things failed, they could feed the same seed back into the random number generator and run the test repeatedly until they found the bug. They also ensured that their tests ran the same set of sequences every time to avoid the problem of flaky tests.

Lastly, Nathan acknowledges the need to limit the number of combinations testing tries. In his experience, once complex tests with a great deal of randomization have been run tens of thousands of times and given green after a few hours have passed, then you can trust that it will continue to pass the test.

The future of ZedZed is still in its early stages and has a long roadmap of features ahead, being Copilot integration and language support the most demanded. Nathan has in mind a modality of Zed that would work as some sort of mob programming of sorts in the way of an interactive Twitch stream, where you can code and the audience can participate. In this same regard, Zed could also have a microphone to allow people to chat. Also, Nathan has in mind a working hours feature in which you can indicate when you want to chat and when you don’t and want to focus on your work alone.

Currently, Zed does not offer support for add-ons. While it has been asked, Nathan believes it is important first and foremost to ensure the quality of the product. While he believes that Zed will support add-ons eventually, he also understands that once you rely on third parties, you lose control in terms of quality assurance: “Right now inside of Zed, if something isn’t making sense, I can just change it. And as soon as you have other people building on top of what you’ve built, you can’t do that anymore.”

The bottom lineDownload Zed open beta and try it for yourself. You can help developers by sharing your feedback: either through a feedback tool in the editor or in the GitHub repository. Currently, Zed only supports macOS. However, there are open discussions for Linux, web, and Windows support. Visit zed.dev to learn more about the app and the team behind it.

You can follow Nathan Sobo on Twitter and GitHub.

The post Nathan Sobo on The Future of Collaborative Code Editing appeared first on Semaphore.

View Details

ChatGPT has stormed the internet in the last four months with its capabilities to understand and generate natural language with remarkable fluency and accuracy. OpenAI, the creator of ChatGPT, recently released a ChatGPT API that enables you to create a wide range of applications that require natural language processing, like chatbots, virtual assistants, and more.

This article outlines how you can take advantage of these powerful AI models and create apps using Flutter and Dart.

Overview of ChatGPTChatGPT is a large language model that performs language processing tasks like language modeling and text generation. ChatGPT was trained on a vast training data set from different sources, including the web. Despite the fact that its training data only goes to 2021, it is still able to answer my queries correctly most of the time:

Although, you should be mindful of the accuracy of the responses, because ChatGPT might give you confidently incorrect answers, as shown below:

My experience is that you need to be specific with your prompts to get high-quality responses from ChatGPT. Also, you have to be knowledgeable to comprehend the responses.

ChatGPT also has a ChatGPT Plus subscription where you can access ChatGPT during peak hours, get faster responses, and try out new features like GPT-4.

ChatGPT vs other OpenAI modelsChatGPT is one of the several large language models built and maintained by OpenAI, which include Ada (1.2b parameters), Babbage (1.5b parameters), Curie (2.7b parameters), and Da Vinci (175b parameters). The term “parameters” refers to the number of weights the model learned during the training process.

ChatGPT and OpenAI’s other language models differ in training data size, capabilities, speed, and cost. ChatGPT has around 6 billion parameters and was made specifically for general language processing.

You can fine-tune each OpenAI language model to your specific needs or domain.

Creating an OpenAI API keyYou will need an OpenAI API key to access OpenAI APIs via RESTful APIs. Create a new account and add your credit card to get $5 in free credits during your first 3 months. OpenAI has a pay-per-use pricing model. See OpenAI’s pricing for more details.

  1. Go to the OpenAI website at https://platform.openai.com/overview.
  2. Click on the “Sign Up” button in the top right corner of the page.
  3. Fill out the registration form with your name, email address, and password. You must also agree to the OpenAI terms of service and privacy policy.
  4. Once you have registered, log in to your OpenAI account.
  5. Open the page for the API keys at https://platform.openai.com/account/api-keys.
  6. Click on the “Create new secret key” button.

  7. OpenAI Developer Platform

  8. Copy the API and store it securely.

Integrating ChatGPT with FlutterFlutter is a multi-platform UI toolkit that lets you create apps for almost any screen, whether mobile, web, or native desktop. Flutter helps you to stay productive while still being able to create functional and beautifully-designed apps.

We’ll integrate ChatGPT into a Flutter iOS app using Flutter 3.7 stable SDK. Please read the macOS install guide for Flutter to set up and debug the app using iOS simulators.

ChatGPT SDKFlutter has a community-based SDK wrapper for OpenAI’s APIs to help you get up to speed integrating ChatGPT into your Flutter apps.

Setting up a Flutter projectFirst, clone (or download the zip) the project from: https://github.com/joshuadeguzman/echo_ai.

git clone <https://github.com/joshuadeguzman/echo_ai> Next, create a file .env in the assets directory.

Next, in the assets/.env file, add YOUR_API_KEY with your OpenAI API key:

OPENAI_API_KEY=<YOUR_API_KEY> Last, run the project using VS Code extension or terminal:

flutter run Integrating ChatGPT SDKLet’s see how we can add ChatGPT to our Flutter app. We’ll add features to send requests and process user prompts using OpenAI APIs.

Handling the Initial Response

First, let’s initialize the ChatGPT SDK. In the lib/chat.dart, add the following code to the initState():

// lib/chat.dart@overridevoid initState() { _messages = []; _isLoading = false; // Initialize ChatGPT SDK _openAI = OpenAI.instance.build( token: dotenv.env['OPENAI_API_KEY'], baseOption: HttpSetup( receiveTimeout: const Duration(seconds: 30), ), ); // This tells ChatGPT what his role is _handleInitialMessage( 'You are a ${widget.character.toLowerCase()}. Please send a super short intro message. Your name is Echo.', ); super.initState();} Next, inside the _handleInitialMessage method, let’s handle the initial message request and response:

// lib/chat.dartFuture<void> _handleInitialMessage(String character) async { setState(() { _isLoading = true; }); final request = ChatCompleteText( messages: [ Map.of({"role": "assistant", "content": character}) ], maxToken: 200, model: kChatGptTurbo0301Model, ); final response = await _openAI.onChatCompletion(request: request); ChatMessage message = ChatMessage( text: response!.choices.first.message.content.trim().replaceAll('"', ''), isSentByMe: false, timestamp: DateTime.now(), ); setState(() { _messages.insert(0, message); _isLoading = false; });} Hot reload to reflect the changes.

Tap Product Manager from the Home Page menu. You should see a welcome message from ChatGPT, as shown below:

Handle User Response

First, in the _handleSubmit method, add the following to handle the user prompts:

// lib/chat.dartFuture<void> _handleSubmit(String text) async { setState(() { _isLoading = true; }); _textController.clear(); // Add the user sent message to the thread ChatMessage prompt = ChatMessage( text: text, isSentByMe: true, timestamp: DateTime.now(), ); setState(() { _messages.insert(0, prompt); }); // Handle ChatGPT request and response final request = ChatCompleteText( messages: [ Map.of({"role": "user", "content": text}) ], maxToken: 200, model: kChatGptTurbo0301Model, ); final response = await _openAI.onChatCompletion(request: request); // Add the user received message to the thread ChatMessage message = ChatMessage( text: response!.choices.first.message.content.trim(), isSentByMe: false, timestamp: DateTime.now(), ); setState(() { _messages.insert(0, message); _isLoading = false; });} Next, inside the _buildChatComposer method, add the following to call the prompt submission:

// lib/chat.dartWidget _buildChatComposer() { return Container( margin: const EdgeInsets.symmetric( horizontal: 8, vertical: 12, ), child: Row( children: [ Expanded( child: TextField( controller: _textController, decoration: InputDecoration.collapsed( hintText: 'Type a message', enabled: !_isLoading, ), // Add this to handle submission when user presses done onSubmitted: _isLoading ? null : _handleSubmit, ), ), IconButton( icon: const Icon(Icons.send), // Add this to handle submission when user presses the send icon onPressed: _isLoading ? null : () => _handleSubmit( _textController.text, ), ), ], ), );} Hot reload to reflect the changes.

Enter a question prompt and tap send, and you’ll get the following:

That’s it! You have integrated ChatGPT into a Flutter app. Now, let’s try to set up a CI/CD workflow for our app using Semaphore.

Deploying a Flutter App using SemaphoreSemaphore is a CI/CD (continuous integration and delivery) platform that allows developers to automate workflows for build, test and deployment phases. Read this guide to get started with Semaphore.

We’re going to create and configure our workflows using Semaphore’s Visual Builder.

Configuring the workflow for the projectFirst, in the navigation bar, click “Create New +” to create a new project.

Then, select the repository of your project.

Last, click “Customize” to manually set up your workflows.

Setting up the continuous integration workflowWe will set our main pipeline to use a Mac-Based Virtual Machine environment with a macos-xcode14 image.

Let’s start our workflow by creating our main block.

Now, let’s install and cache Flutter.

checkoutexport PUB_CACHE=$PWD/.pub-cachecache restore flutter-packages-$SEMAPHORE_GIT_BRANCH-$(checksum pubspec.yaml),flutter-packages-$(checksum pubspec.yaml),flutter-packagesflutter pub get cache store flutter-packages-$SEMAPHORE_GIT_BRANCH-$(checksum pubspec.yaml),flutter-packages-$(checksum pubspec.yaml),flutter-packages .pub-cachecp assets/.env.example assets/.env

Next, let’s write our jobs for the lint block:

Format

flutter format --set-exit-if-changed . Analyze

flutter analyze . Prologue

checkoutexport PUB_CACHE=$PWD/.pub-cachecache restore flutter-packages-$SEMAPHORE_GIT_BRANCH-$(checksum pubspec.yaml),flutter-packages-$(checksum pubspec.yaml),flutter-packagesflutter pub get cache store flutter-packages-$SEMAPHORE_GIT_BRANCH-$(checksum pubspec.yaml),flutter-packages-$(checksum pubspec.yaml),flutter-packages .pub-cachecp assets/.env.example assets/.env Last, add a test block to run our unit tests.

Test

flutter test test Prologue

checkoutexport PUB_CACHE=$PWD/.pub-cachecache restore flutter-packages-$SEMAPHORE_GIT_BRANCH-$(checksum pubspec.yaml),flutter-packages-$(checksum pubspec.yaml),flutter-packagesflutter pub get cache store flutter-packages-$SEMAPHORE_GIT_BRANCH-$(checksum pubspec.yaml),flutter-packages-$(checksum pubspec.yaml),flutter-packages .pub-cachecp assets/.env.example assets/.env Amazing. You have successfully built your continuous integration pipeline. You’re now ready to automate your deployment with Semaphore.

For more, you can read our article on Automate Flutter App Deployment on iOS to TestFlight using Fastlane and Semaphore to deploy your apps to TestFlight. We also have articles on how you can deploy Flutter iOS apps and Flutter Android apps on Firebase.

ConclusionChatGPT has a lot of potential for new business ideas and improving existing ones. You can leverage ChatGPT models for your Flutter apps to create chatbots, SaaS applications, and much more. You can fine-tune your models to accomplish specific tasks that meet your business requirements with better performance and accuracy. Finally, you can deploy the code changes and your Flutter projects using Semaphore for reasonable cost and with faster performance.

The post Build a ChatGPT-Powered Chatbot With Flutter appeared first on Semaphore.

View Details

Cloud computing is the great equalizer of the modern workplace. With it, even the smallest of businesses can punch way above its weight. However, due to their smaller IT and cybersecurity teams, they’re also more at risk, which raises some important issues.

  • Its risk assessment and fraud prevention capacities are off the charts, but how significant is this?
  • Perhaps even more importantly, are there any downsides?
  • Compared to the opportunities, do these downsides give a net positive or negative?

Stick around, and we’ll try to answer how even the smallest of enterprises can handle these issues with modern cloud computing capacities and threat analysis methods.

Source: UnsplashBetter agility and scalabilityCloud agility refers to issues like monitoring, analysis, and security. As such, it’s tightly intertwined with risk management and fraud prevention.

The biggest problem with the toolset of a modern business is that it’s constantly evolving. This means that you’re currently using a tool that you may replace or upgrade in the future. It also means that you might introduce new tools to complement your work process.

While all of this sounds great, the problem is that you change a delicate ecosystem with everything new you introduce. A single unoptimized or untrustworthy tool can compromise a dozen other great tools at your disposal.

Cloud agility provides you with greater flexibility and automated allocation of resources. This way, your team can focus on processes that require human input. Since the majority of these tasks are mostly technical, automation lowers the likelihood of error.

Regarding scalability, cloud computing allows businesses to buy more computing power without dealing with hardware. You don’t have to make more space for the hardware. You don’t have to install it. You don’t even have to wait for it to get delivered. You just pay for it and get to use it (usually) in minutes.

Scalability is especially important when growing a small team.

Enhanced overall securityOne of the most marketable promises of cloud computing is transparency regarding your cloud-based assets. This means that you get 24/7 monitoring of everything that’s going on. Even when you’re not actively tending to your assets, you can rest assured that they are under a watchful eye.

The most significant advantage of cloud computing security is the advanced protection against DDoS attacks. This is a huge problem since it is estimated that an hour of a DDoS attack can cost an organization as much as $40,000.

The biggest addition to your risk management efforts comes from advanced threat detection. This usually includes tools and methods like:

  • Endpoint scanning: Here, the system will check the integrity of each IP device before allowing it to connect to your network. So, if a device from this IP has made previous offenses, their activity would raise the alarm.
  • Global threat intelligence: The system will check the integrity of the IP device before allowing it to connect to your network. The most challenging thing they do is provide contextual metrics, which help you understand the threat level based on the user’s location.

Most importantly, great cloud security helps you fulfill regulatory standards and compliance needs. As a business, application security will be one of the things most closely monitored by regulatory bodies. Cloud computing can help make this a lot easier.

Source: UnsplashHackers aren’t the only ones committing fraudHackers and malicious third parties are not responsible for all frauds. Sometimes, this fraudulent behavior comes from people you’re doing business with. They’re not doing it under a false identity; no hackers are involved. Either your buyer or the business you’re buying from is defrauding you.

So, what is friendly fraud?

Simply put, it’s fraud committed by one of the parties in a transaction, and it often looks like a mistake. A client makes an order for an item and later files a claim that they didn’t make an order or that someone stole their credit card. It’s an opportunist behavior that’s hard to prove.

Still, not all friendly frauds are malicious. A customer can genuinely forget about making an order and request a refund claiming that they didn’t make it.

It’s not always the customer’s fault either. Sometimes, a merchant may make a mistake or miss a delivery (when it’s time-sensitive).

The most common form of friendly fraud comes in the form of policy abuse fraud. If a merchant has a return policy that’s too forgiving, a customer might test out a product or use it once before returning it.

The simplest way to handle friendly fraud is to request confirmation during the purchasing process. This can be in the form of a CVV number, typing in a code they received via an SMS, or asking for a confirmation email. Once they confirm, the customer can no longer claim it wasn’t them. Likewise, if a merchant doesn’t receive a confirmation for a purchase, they can discontinue the transaction.

From the merchant’s standpoint, excellent technical safety (a common trait of the cloud computing process) will reduce the risk of errors. Moreover, with the cloud, there’s elaborate record-keeping in place. This means it’s easier to prove who made an “error.”

Challenges of cloud computing Of course, cloud computing is not without its downsides, some of which are pretty serious.

Data privacyOne of the biggest misconceptions is that consumers’ private data is, somehow, less safe because it’s in the cloud. There are several reasons why this argument is dubious.

First of all, less safe compared to… what exactly? Consumer data has always existed, and it holds up compared to paper documents. About 7.5% of all paper documents end up lost. Some of which might even end up in the wrong hands. With cloud computing, the numbers are nowhere near this.

The biggest concern regarding data confidentiality is that customer data shouldn’t be available to unauthorized personnel. Still, in cloud computing, you have a high level of access customization to help you handle this issue.

Regarding data loss, we’ve already mentioned that the numbers will always look better than the old-school alternative. Data recoverability rates are far higher than if you were to keep information on the HDD in your office.

The most reasonable concern for data security is that of geographical storage issues. Servers are sometimes abroad, meaning they are outside your local jurisdiction. This is a major concern that has yet to be solved.

Source: UnsplashIntegration with legacy systemsOne of the biggest problems with cloud computing is that it works best when all software is up to date. Sounds simple, right? Just update your software when needed. The problem with this is that some businesses prefer legacy systems or don’t have hardware supporting the superior version.

In fact, there are many reasons why companies maintain legacy systems. For instance, change is hard. Just try to remember the last few changes to the Facebook interface. Not many people liked it, even though they couldn’t articulate what they disliked about it. Nostalgia is a powerful motivator, and resistance to change is a natural human reaction.

Also, upgrading to a new system can be expensive. In the long run, legacy software will cost you more, but upgrading usually requires an upfront investment. Upgrading is always a good choice, but it’s a choice that many entrepreneurs won’t make unless forced to.

The downsides of these problems are more severe than you may initially assume. First, compliance is much more complicated if you’re running a legacy system. Second, maintenance becomes more difficult as time goes on. At one point, you’ll have to upgrade or make a switch. Postponing the inevitable is not a sensible choice.

Finally, and most importantly, security is weak and weakening. This is the problem that will scale up in the future.

Dependence on cloud providersOne of the most significant concerns many businesses have is being too cloud-dependent. While traditional computing has numerous downsides, it always felt more independent than its cloud alternative.

After all, your hardware is in your office, and you install all your software on these devices. Moreover, in the past, most software required a single-purchase license (compared to the modern SaaS approach). Naturally, this has business owners worried that they might become too cloud-dependent.

Many cloud providers do not accept liability for data loss, which exacerbates the issue. This is mostly out of fear that they could be liable for the flaws in third-party apps and services.

However, big cloud providers have data servers worldwide, ensuring that a single-point failure is not catastrophic for the client.

Conclusion: cloud computing can make your risk management more reliable and your business saferDespite fears and shortcomings, cloud computing is a huge risk management and fraud prevention asset. Increased computing capacities make threat analysis fast and accurate. Moreover, the fact that the network is global means it can check a suspicious IP and determine if it was previously involved in suspicious behavior. By preventing such access, you’re already increasing the overall security.

Sure, there are still some concerns regarding dependency, data privacy, and compliance, but for the time being, the result of this tech trend is overwhelmingly positive.

The post The Impact of Cloud Computing on Risk Management and Fraud Prevention appeared first on Semaphore.

View Details

This is part 3 of a series of articles on the importance of Agile methodology in IT. This series is divided into 3 parts:

  1. Benefits of Agile Methodology
  2. How Agile Makes Programming Teams Succeed
  3. Strategies for Agile Implementation

In parts 1 and 2, we covered how, if successfully implemented, Agile benefits teams and helps them succeed. To wrap up, let’s cover how to successfully implement Agile into your workflow.

Due to their success in enhancing team cooperation, productivity, and efficiency, Agile and Scrum methodologies are gaining popularity in the software development sector. Unfortunately, putting Agile and Scrum into practice is not always easy and can be a complicated process. Companies must create defined roles and responsibilities, efficient communication channels, and a continuous improvement culture in order to successfully implement Agile and Scrum.

Creating a product backlog, dividing the work into manageable chunks, holding frequent retrospectives and reviews, and honing cross-functional abilities are just a few of the tactics we’ll cover in this article for adopting Agile and Scrum in software development teams. Teams can effectively switch to Agile and Scrum methodologies and make use of their advantages by adhering to these tactics.

Strategies for Agile Implementation: OverviewAgile and Scrum can help define clear roles and responsibilities within a development team. Here are some ways to define clear roles and responsibilities using Agile and Scrum methodologies.

  1. Define clear roles: one of Scrum’s core principles is clearly defining the team’s functions. The Scrum framework includes three leading roles: Product Owner, Scrum Master, and Development Team. By clearly defining these roles and responsibilities, teams can ensure that everyone knows what is expected of them.
  2. Cross-functional teams: another way to clearly define roles and responsibilities is to create cross-functional teams. In Agile, teams are made up of people with different skill sets who work together to complete a project. A cross-functional team allows everyone to know their specific responsibilities, and the team as a whole is better able to tackle complex problems.
  3. Periodic inspection: regular reviews like daily meetings can also define clear roles and responsibilities. During these meetings, team members can share what they have worked on and plan to work on next. This helps identify differences in responsibilities and ensures that everyone is working towards the same goal.
  4. Clear Communication: clear communication is essential when defining roles and responsibilities. By communicating openly and regularly, team members can ensure that everyone is on the same wavelength and that there is a clear understanding of who is responsible for what. In addition, it allows the work to be completed on time and with a high level of quality.

Establish clear objectives and metricsAgile and Scrum methodologies help establish clear objectives and metrics for a development project. The team collaborates to create a product backlog, and a prioritized list of features that the team will work on over the course of the project. Then, each component is broken down into smaller, more manageable tasks that can be completed quickly, usually in a few weeks. At the end of each iteration, the team reviews the completed work and assesses whether it meets the objectives and metrics set for that iteration. If not, the product backlog can be adjusted and the team can work towards a new goal in the next iteration.

Similarly, teams work in short sprints in Scrum to complete tasks. The team sets clear goals and metrics for each sprint, and at the end of the sprint, the team reviews the work completed and evaluates whether the goals and metrics are being met. This allows teams to adjust and continuously improve their work over time.

These methodologies help in regularly reviewing and adjusting the process in the following ways:

  1. Frequent retrospectives: Scrum and Agile methodologies incorporate regular retrospectives to review completed work and identify improvement areas. These retrospectives are typically held at the end of each sprint or iteration to allow the team to reflect on their progress and make adjustments to the process.
  2. Continuous feedback: Agile and Scrum methodologies also emphasize the importance of continuous feedback. By getting input from stakeholders, team members, and customers, teams can identify areas where the process can be improved and make real-time adjustments.
  3. Agile mindset: adopting an Agile mindset means embracing the idea of continuous improvement. In an Agile environment, teams are encouraged to try new things and experiment with different approaches to see what works best. This means that the process constantly evolves and improves, allowing the team to adapt to changing circumstances and improve over time.
  4. Transparency: Agile and Scrum methodologies emphasize the importance of transparency. Transparency into processes and progress allows teams to identify areas for improvement and work together to make necessary adjustments.

Training and education deliveryScrum and Agile put focus on the importance of collaboration and teamwork. As a result, participants can learn from each other and share knowledge and experiences working as a team. When discussing the practical working environment, team members are encouraged to develop skills in multiple areas. This allows team members to acquire new skills and expand their knowledge to do their jobs more efficiently.

Both Agile and Scrum methodologies also include the use of learning and coaching. Teams are trained on new tools, techniques, and methodologies to help them work more effectively and efficiently. Coaching can also help team members develop skills and improve performance.

Encouraging open communication and feedbackAgile and Scrum methodologies encourage open communication and feedback within the team in the following ways:

  1. Daily Stand-Up Meetings: teams conduct daily stand-up meetings where members share updates about their progress, discuss any roadblocks, and identify opportunities for collaboration. This promotes open communication and helps team members stay informed about each other’s work.
  2. Sprint Retrospectives: at the end of each sprint, teams hold retrospectives where team members reflect on the sprint, discuss what went well and didn’t go well, and identify areas for improvement. This allows team members to provide feedback to one another in a constructive way and encourages open communication.
  3. Product Backlog Refinement: teams will conduct product backlog refinement meetings where they can review the product backlog and discuss any changes that need to be made. This promotes open communication and helps team members to stay aligned regarding project goals.
  4. Collaboration Tools: teams use collaboration tools, such as Jira or Trello, to track progress, assign tasks, and communicate. These tools promote open communication by allowing team members to share updates, ask questions, and provide real-time feedback.

Building a Culture of Continuous ImprovementAgile and Scrum methodologies help teams and companies build a culture of continuous improvement by encouraging a mindset of reflection, feedback, and collaboration. In addition, by breaking work into smaller, manageable pieces, these two methods enable teams to evaluate their progress and make necessary adjustments regularly. Sprint retrospectives, daily stand-ups, and product backlog refinement sessions create a space for open communication and feedback, allowing team members to share their insights and collaborate on improving processes.

Applying both methodologies encourages a willingness to take risks, experiment with new approaches, and adapt to changing circumstances. This culture of experimentation and adaptation allows teams to continuously learn from mistakes and make incremental improvements to increase product quality and customer satisfaction.

Ultimately, the aforementioned methodologies promote a culture of continuous improvement that values ​​transparency, collaboration, and a willingness to continually change and improve.

ConclusionThe Agile and Scrum approaches provide a number of methods for clearly defining roles and responsibilities, setting goals and KPIs, encouraging cooperation and collaboration, and creating a culture of continuous development. The product backlog can be used by teams to prioritize work, divide it up into smaller, more manageable projects, and work in short sprints with defined targets and KPIs to assess progress and make adjustments.

Team members will build skills in many areas, exchange information and experiences, and engage in open communication and feedback sessions. Teams should adopt an attitude of flexibility, adaptation, and continuous learning in order to successfully apply these tactics. They should also customize their approaches to their own needs and circumstances.

The advantages Agile and Scrum offer are well worth the effort for teams trying to optimize their development operations, even though their implementation could necessitate some changes to current procedures.

The post Strategies for Successful Agile Implementation appeared first on Semaphore.

View Details

This is part 2 of a series of articles on the importance of Agile methodology in IT. This series is divided into three parts:

  1. Benefits of Agile Methodology
  2. How Agile Makes Programming Teams Succeed
  3. Strategies for Agile Implementation

In part 1, we covered the benefits of Agile Methodology, from how Agile improves communication to impact on real-time feedback. In this part, we will cover how Agile helps programming teams succeed.

The effectiveness and efficiency of programming teams’ collaboration are key factors in determining their success. With its focus on cooperation, collaboration, and iterative development, the Agile Methodology offers a defined framework for development. Instead of depending on conventional top-down management techniques, it gives team members the freedom to own their work and make choices together.

The Agile Methodology promotes a sense of community and camaraderie within the development team by helping to dismantle conventional hierarchies and power systems. When talking about this approach in cooperative IT culture, it is worth mentioning that it has a proven record of helping teams deliver good results, with products that can be easily altered throughout the development phases in response to changing circumstances.

Let’s examine, in depth, how the Agile approach affects team dynamics and morale, productivity and efficiency, as well as quality and consistency in this post.

How Agile Methodology Affects TeamsAgile skills significantly impact team dynamics and morale, often resulting in more positive and effective team interactions. The Agile Methodology emphasizes teamwork and collaboration, helping to foster a sense of community and camaraderie within the development team. It also encourages team members to collaborate, communicate frequently, and share knowledge and experiences.

Team members often feel more engaged and invested in the project as a result, which can boost their morale and motivation. By empowering team members to take ownership of their work and make decisions as a team, this philosophy helps break down traditional hierarchies and power structures.

It also puts a focus on regular feedback and collaboration, which can help build trust and transparency within the team. By encouraging open and honest communication, Agile helps create an environment where team members feel comfortable giving and receiving feedback.

Impact on Team Productivity and EfficiencyAgile has a significant impact on team productivity and efficiency. This is so because the methodology, at its core, emphasizes iterative and collaborative development, continuous feedback, and flexible project management, which can benefit team productivity and efficiency.

The iterative nature of Agile Methodology means that programming teams have the option of modifying the backlog throughout the development process. When Agile is used properly, it can also help identify potential roadblocks and obstacles early in the development process, which can be addressed more quickly, leading to increased productivity and faster delivery.

Programming teams can release their products to market more quickly thanks to the Agile Methodology, which is meant to help teams create functional software quickly and often. In the hectic corporate world of today, this can be a big advantage.

Impact on Team Communication and CollaborationCompanies that adopt Agile Methodology provide regular opportunities for team members to work together through daily stand-up meetings, sprint planning sessions, and retrospective meetings. This frequent collaboration helps to build trust and transparency within the team, leading to more effective and efficient communication.

Continuous feedback is a key component, coming from both stakeholders and the team itself. The team can make changes and modifications as needed with the aid of this input, which provides an understanding of what is functioning well and what needs to be improved.

In addition, by encouraging team members to work together and share their knowledge and expertise, Agile Methodology helps to create a more collaborative and productive team environment, ultimately leading to more successful projects and higher job satisfaction for team members.

Impact on Team Quality and ConsistencyAgile and Scrum methodologies significantly impact team quality and consistency by providing a structured framework for development that encourages a focus on quality and consistency throughout the development process. Scrum methodology, in particular, emphasizes regular feedback, continuous improvement, and repetitive development, which can help to identify and address issues early on in the development process, leading to higher-quality outcomes.

This enables teams to concentrate on one focused set of features or tasks at a time, which can lower the risk of mistakes and inconsistencies. Large projects are broken down into smaller, more manageable sprints. This specific methodology encourages team members to work together, share their knowledge, and support one another to make sure that the final result is of high quality and consistent throughout by highlighting the significance of cooperation and teamwork.

Both Agile and Scrum provide regular opportunities for retrospective and process improvements, which can help teams to evaluate and improve their processes continuously, leading to higher-quality outcomes and more consistent development practices.

Impact on Team Adaptability and FlexibilityScrum, in particular, emphasizes the importance of flexibility and adaptability by providing a framework for regular and continuous feedback, collaboration, and process improvements. Teams are encouraged to work together to identify potential roadblocks and obstacles early in the development process and adjust their approaches as needed to ensure that they meet customer needs and deliver high-quality outcomes.

Scrum and Agile support teams in better responding to changing requirements and customer needs by emphasizing frequent feedback, collaboration, and adaptability. These methods allow teams to be more adaptable, leading to faster delivery and more successful projects.

Furthermore, this can help teams be more flexible in their approach to work, as they can adjust their processes and priorities as needed to ensure that they meet customer needs and deliver high-quality outcomes.

ConclusionThe success of programming teams can be strongly impacted by the Agile Methodology, which is a potent strategy. The Agile approach may support the creation of a healthy team culture that encourages innovation, creativity, and excellence by placing an emphasis on cooperation and collaboration, giving team members agency, and offering a defined framework for development.

Agile Methodology’s iterative and collaborative approach to development can increase productivity and efficiency by allowing teams to identify and rapidly fix problems. Regular cooperation and feedback also foster a climate of openness and trust, which can improve communication and eventually result in higher-quality results. The Scrum methodology, in particular, highlights the value of adaptation and flexibility by offering a framework for ongoing collaboration and process development.

Programming teams can use this method to discover possible problems early on and modify their strategies as necessary to fulfill client expectations and provide quality results.

The post How Agile Helps Programming Teams Succeed appeared first on Semaphore.

View Details

This is the first part in a series of articles on the importance of Agile Methodology in IT. This series is divided into three parts:

  1. Benefits of Agile Methodology
  2. How Agile Makes Programming Teams Succeed (coming soon)
  3. Strategies for Agile Implementation (coming soon)

The Agile Methodology is an iterative approach to software development that emphasizes flexibility, collaboration, and customer satisfaction. Companies that utilize Agile techniques prioritize delivering working software in small chunks rather than providing a complete solution simultaneously.

Agile is best known for its use in software development projects where requirements are uncertain or subject to change. It emphasizes close collaboration between developers, customers, and other stakeholders and encourages regular communication and feedback. Although it has gained popularity with informational technology teams, one can use it in a wide range of industries that are not closely related to software development.

Agile involves breaking down development into short iterations, the exact length of which can vary depending on the project and team requirements. Successful companies use Agile to work closely with customers to define and prioritize requirements, because it helps them create right-sized products (or products that are “good enough for now”). Consequently, if teams stay true to Agile’s iterative philosophy, it can result in the creation of a high-quality product over many iterations.

Teams use various tools and practices to facilitate collaboration and communication to evaluate progress and identify areas for improvement. Scrum is a project management framework based on Agile Methodology. It focuses on collaboration, flexibility, and iterative development to help teams create right-sized products or products that are appropriate for the time being. At its core, Scrum represents the division of a project into smaller, more manageable chunks called sprints, typically lasting one to four weeks. The team works together during the sprint, starting their day with a short, humble stand-up meeting to ensure that progress is on track. After completion, a Sprint Review presents the current status of the product, inspects and adapts the upcoming increments, gathers feedback processes, and identifies areas for improvement.

Scrum helps teams by providing a clear structure for project management, encouraging collaboration and communication, and allowing for continuous improvement based on team and stakeholder feedback.

When we talk about Agile, there are other frameworks, such as Kanban and Lean. However, due to the popularity of the Scrum framework, it will be this article’s focal point.

Benefits of Agile MethodologyAgile methodology provides increased flexibility and adaptability because it is designed to handle the changes and uncertainties common to many software development projects.

One of the key features of Agile is its iterative approach to development. Instead of delivering a complete solution, development is broken into smaller, more manageable pieces that can be shipped and tested in short iterations.

This approach allows teams to react to changing requirements and feedback in real-time rather than waiting until the project is finished.

Improving communication and collaborationAgile also encourages a culture of continuous improvement and learning.

This approach emphasizes the ability to adapt to change and respond quickly to feedback, enabling development teams to deliver value to customers faster and more efficiently. Therefore, retrospectives in the Scrum framework are critical since they provide continuous improvement and reflection. Teams are encouraged to reflect on their processes and adjust to optimize performance and deliver better results.

This puts focus on continuous learning and improvement, enabling development teams to respond more easily to changing circumstances and customer needs. The increased flexibility and adaptability offered by Agile Methodology enable development teams to be more responsive to changing requirements and deliver value to customers more quickly and effectively. Teams can also apply Agile Methodology to projects requiring iterative development, continuous feedback, and collaboration.

This approach emphasizes continuous feedback and collaboration, which can help teams identify issues and challenges more quickly and make adjustments as needed. Due to the fact that there are regular opportunities for team members to share their work, receive feedback, and collaborate on solutions, Agile Methodology helps reduce the risk of delays and errors, ultimately leading to more productive and efficient development processes.

Faster delivery of working softwareIn terms of practicality and efficiency, the Agile Methodology allows teams to ensure faster delivery of working software by emphasizing collaboration, iterative development, and a focus on delivering value to customers promptly.

Critical aspects of Agile that contribute to more rapid delivery of software include:

  1. Iterative Development: Agile breaks development into smaller iterations, usually between 1–4 weeks. Iterative development allows teams to focus on delivering a working solution incrementally rather than waiting until the end of the project to provide the entire solution. This approach enables teams to respond quickly to changing requirements, test and validate solutions, and deliver customer value faster.
  2. Continuous Integration and Delivery: Agile also emphasizes continuous integration and delivery, which means that code is integrated and tested continuously through development.

In Scrum and Agile technology, integrated code refers to the procedure of combining individual team members’ code modifications into a single codebase (using a software versioning tool, e.g. Git) that can be tested and released as a functional software product. Integrated code ensures that everyone on the team is working with the most recent version of the codebase, preventing conflicts and discrepancies between various versions.

Moreover, because Agile and Scrum support continuous integration and testing, any problems or errors may be found and corrected early on in the development cycle, lowering the likelihood that serious difficulties will occur later. This approach ensures that teams can identify and resolve defects or issues quickly, enabling them to deliver working software more frequently.

Increased customer satisfactionThe whole concept of Agile Methodology is centered on delivering customer value and emphasizes customer involvement throughout the development process. The customer actively defines requirements, provides feedback, and regularly reviews progress. The involvement of customers ensures that the software under development meets the customer requirements and that any issues can be addressed early.

However, involving the customer in every development process step is only occasionally feasible or practical. In most cases, a Product Owner is assigned to work with and/or lead the development team and represent the customer’s interests.

Real-Time feedback and adjustmentsCompanies that fully utilize Agile Methodology are already well-acquainted with daily stand-up meetings, which allow the development team to discuss progress, identify issues, and adjust as needed. For those new to Agile, these meetings are usually short and allow the team to provide real-time feedback on the project’s progress and issues that need to be addressed.

Daily meetings are a vital component of Agile and are essential in providing real-time feedback and coordination within the project. These meetings are usually short (15 minutes or less) and are held at the same time each day, usually in the morning. These meetings are done standing because they are intended to make the real-time feedback and coordination process as short as possible, since many folks are uncomfortable standing for more than fifteen minutes. Therefore, it is the scrum master’s job to hold the team to the time limit and keep the focus on the vital parts of the meeting and, consequently, of the whole project.

Concerning the aforementioned issue, it is essential to note that stand-up meetings are not unique to Agile Methodology and can be used in other project management frameworks. The purpose of these meetings is to allow the development team to discuss progress, identify issues, and make necessary adjustments.

During the daily stand-up, each team member answers three questions.

  1. What work did you finish yesterday?
  2. What work are you going to finish today?
  3. What is preventing you from finishing your work?

By answering these questions, team members report project progress in real-time, identify issues or problems that need to be addressed, and communicate with other team members about their work. This ensures everyone is on the same page and working towards the same goal. In addition, daily stand-up meetings allow the development team to adjust as needed.

Once team members identify a problem, the team can work together to find a solution and adjust the project plan as needed. This allows the team to adapt quickly and ensure that the project is progressing as planned.

Emphasis on teamwork and self-organizationAgile techniques emphasize teamwork and self-organization as fundamental principles of the methodology. In an Agile environment, the development team can take responsibility for the project and make decisions. Rather than relying on a traditional top-down management approach, this approach encourages team members to collaborate, communicate, and work together to achieve the project goals.

The focus is on teamwork, and collaboration is reinforced by the Agile Methodology’s iterative development approach, which involves regular meetings and frequent feedback on progress. This approach also emphasizes self-organization, which means that the development team has the autonomy to decide how to approach their work and the flexibility to adapt as needed.

Although there is a high level of autonomy within the team, one vital role that needs to be further explained is the Product Owner. The Product Owner is the principal stakeholder who represents the client or end-user and is in charge of managing the product backlog. Working closely with other project participants, such as the Scrum Master and the development team, is a crucial part of the Product Owner’s job. According to Scrum and Agile methodologies, the Product Owner is responsible for defining and prioritizing the product backlog, which is a list of features, improvements, and flaws that the team must fix.

In practical terms, Agile and Scrum allow the team to communicate with customers to show them how the project progresses incrementally. Also, the presence of a Product Owner means that customers’ needs are being faithfully represented. This is beneficial since customers can express their needs and comment on the work. In the long term, it saves time and improves communication.

ConclusionCompanies need to be able to swiftly react to shifting client desires and market situations in today’s fast-paced business environment. The Agile Methodology offers an iterative approach to software development that stresses flexibility, collaboration, and client satisfaction as a solution to this problem.

Teams that embrace this approach are able to produce working software gradually rather than waiting until the end of the project to deliver the whole solution, since development is broken down into smaller, more manageable pieces.

The post Benefits of Agile Methodology appeared first on Semaphore.

View Details

The evolution of distributed systems has normalized the benefits of cloud systems. From an end-user perspective, collaborating in real time has to feel smooth and organic. Still, developing one of these applications isn’t as clear-cut. In this article, we hear from Adam Wulf, one of the developers behind the whiteboard application Muse, on the many steps and nuances of building a real-time collaboration platform and the challenges he and his team faced.

Edited transcriptionSince he founded his first software company right out of college over two decades ago, Adam Wulf has steadily jumped from one project to the next, yet always prioritizing working in a small team. In this way, he is now part of the development team behind Muse, an Apple ecosystem whiteboard application for brainstorming and real-time collaboration.

In Adam’s own words, what sets Muse apart from other whiteboard applications is that “it lets you organize your thoughts spatially.” Instead of offering a single whiteboard as a tabula rasa for your content, “you can actually put in small cards that represent other whiteboards” and nest your ideas as you see fit. Adam understands that Muse “is going to be a team real-time digital office space, for a metaphor, where you can just walk in, see what people are working on, you have all of your content there, and you can easily link out to the rest of the company as well” What’s more, it also supports integration with popular work tools such as Slack.

Muse is mainly used in small teams by product and project managers and designers. For this reason, Muse use cases include:

  • Determining how the user experience looks like
  • Figuring out the product pathway and strategy
  • Evaluating the company strategy
  • Collecting customer feedback
  • Studying new features

While Muse started as a single device and user application, the new possibilities and benefits distributed systems had for end users couldn’t be ignored. Likewise, the expectations for whiteboard tools demanded a different product, so Adam and the rest of the development team refactored Muse to support single-user multi-device synchronization first and later on real-time collaboration between users.

Old world and new world: Migrating the databaseAs the first step in the process, they had to migrate the application data to a new database format compatible with synchronization. To store application information offline, the original Muse application used Core Data, a framework for storing application data offline. However, since Core Data didn’t provide built-in support for synchronization, they had to migrate the application data to a new sync database backend.

According to Adam, they had two options for this kind of database migration. They could either reuse it by “ripping everything apart” and putting it all back together hoping it will work, or “make a series of lots of very tiny changes” and slowly and gradually abandon the former framework and move toward a custom-made new one. Adam and their team stuck to the latter option since completely restructuring the codebase and rebuilding it from scratch could result in a messy codebase.

Slowly walking into the new world started by replacing Core Data concrete classes with protocols. Adam explains that they slowly migrated all the Core Data into one particular implementation of protocols in their application. They then gradually implemented a new version of those protocols in the Sync database layer, slowly moving one interface at a time to another until they successfully migrated everything. While Adam admits it was a challenging transition, he recognizes that the slowness of the process made it possible to deal successfully with the complexity of their application.

Shoring up the new databaseOn the other hand, keeping unit testing was also a big part of the process. Because the entire database structure was brand new, there was a risk of bugs in the new system. By thoroughly testing the new Sync layer, developers were confident that the user’s data and system behavior were safe after migration, and that this new layer could perform all the necessary functions required by the database system, including data storage and synchronization.

Additionally, Adam says they needed to acknowledge the different issues that come with synchronization: “In the Core Data world, if you delete content, you can just delete it and it’s physically gone from the database; in the Sync world, it’s a lot harder to do that because I can delete something from my iPad that has synchronized to my Mac, that on my Mac I had moved somewhere else.”

Adam found the solution in implementing a conflict-free replicated data type (CRDT) structure. In distributed systems, CRDTs are a class of data structures where data replicates across multiple nodes that may not always be in sync with each other. CRDTs ensure that the data remains consistent across the system, even in the presence of network partitions or other failures. CRDTs achieve this consistency by guaranteeing that all updates to the data structure are commutative and associative, which means that they can be applied in any order without changing the result; eventually, all nodes will converge to the same state, even if they receive updates in a different order or at different times.

Likewise, when changes are made to Muse data on multiple devices simultaneously, there is a risk of conflicting updates. In other words, if two devices make different changes to the same data at the same time, it is possible that these changes conflict with each other and cannot be easily merged. To avoid such conflicts, data synchronization systems often use a “last writer wins” approach, where the most recent update to the data is considered the “correct” one and all previous updates are discarded.

However, this approach can lead to data loss, as the changes made on the other devices are overwritten and lost forever. In a way, CRDTs work like a clock that can tell which device is the latest to have made changes. However, to work as expected this clock needs to be set at the same time on all devices. In the case a device has the time set in the future, other devices would not be able to make changes until they caught up with that time. What they use is a Hybrid Logical Clock (HLC). Instead of relying on a centralized time to synchronize all devices, a hybrid logical clock determines the order of events by combining the behavior of logical and physical clocks.

Putting identifiers in everythingSynchronization also has to make sure that changes made from different devices don’t nullify each other. This could happen, for example, when a user edits a board from one device offline and then performs additional changes on the same board from another device. To solve this issue, the backend organizes the content through attribute-value pairs that label the data. These allow decomposing objects into more granular components, such as the color or size of a particular element. In this way, changes merge seamlessly, and “if two devices are editing the same object from a user’s perspective, they’re very probably changing different attributes of that object.”

The attribute-value pair design also helps the application deal with the load of having multiple users working simultaneously without crashing or having performance issues. The application backend collects many of these granular changes that affect the same object into a pack that is sent to the server. The server cannot read the data itself, just a label specifying its “scope” which identifies the data as part of the same box, e. g. a board, a PDF document, or an image.

The server knows who the users are, their access and edition permissions, and what devices they are using, but doesn’t know anything about what’s inside the scopes. Hence, the server capacity doesn’t have to deal with the complexity of this data. Besides, developers can introduce new features within the data in those scopes without having to modify the server. Adam affirms that this design choice “has made managing scale on the server a much easier problem to handle and can be completely disassociated from the problems of the Muse application.”

The granularity of attribute-value pairs also helps with version compatibility. Version compatibility ensures that users can work together seamlessly, regardless of the version of the application they are using. This is particularly important in situations where users need to share files or collaborate on projects, as it ensures that everyone can access and work with the same data

On the other hand, the Muse team can’t guarantee that all devices of a user are running the same version of the app. However, they can label the content with TypeCode, a four-byte value that is used to identify different data types in Mac applications. TypeCode specifies what the object is about; e.g. a board, an image, or PDF, and if the application doesn’t know the object’s label, it will ignore it and only load the new data from the data types that it recognizes.

Lastly, the Muse team also uses identifiers in requests and objects to help them debug and map, and trace requests. They also use versioning to keep track of changes in different parts of the system while ensuring that different versions of the application can communicate and work with each other seamlessly. If a device is on an older version of the application and receives data for a newer version, it will not be able to understand it due to the lack of Protobuf definitions. In this case, the device will set aside the newer version data and continue to function with the older version data. The device will not be able to use any new features or functionality that have been added to the newer version of the application until it has been upgraded to the newer version; then, it will retrieve the newer version data and operate as intended.

The bottom lineLearn more about Muse at museapp.com. Adam also takes part in different open-source projects in the iOS and macOS development sphere. For UIBezierPath, a class in the UIKit framework that represents a vector path, he shared with us two tools:

  • PerformanceBezier: a framework that adds caching into UIBezierPath’s default data structure to increase its performance.
  • ClippingBezier: a library that can find intersections points between paths and related operations.

Adam is also responsible for the development of PonyExpress, a Swift package notification library conceived as an alternative to NotificationCenter that implements type safety.

Adam volunteers in the Prison Entrepreneurship Program, which provides entrepreneurship education for felons during their last years of incarceration. Once they are released, they can use these skills to integrate into society.

You can follow the latest of Adam and his work at adamwulf.me.

The post Adam Wulf on Application Refactoring appeared first on Semaphore.

View Details

Micro frontends is one of several architectural approaches that help developers to break down large web applications into smaller, independent parts.

Each micro frontend application represents a self-contained feature or module that can be developed, tested, and deployed separately from the other micro frontend applications.

This article provides a practical guide for organizations wishing to adopt micro frontends. For further reading on micro frontends, read our previous post “Microfrontends: Microservices for the Frontend”.

Slicing the Monolithic ApplicationThe main challenge with adopting the micro frontends architecture isn’t technical. Instead, the discussion often involves the product team and relates to your vision of the application.

Luca Mezzalira defines two ways of slicing monolithic applications:

  1. Vertical slices: slices are defined by URLs, and each page is sliced into its own micro frontend application.
  2. Horizontal Slices: pages are split into multiple micro frontend applications. For example, teams often own a group of applications that cover specific flows.

To demonstrate the complexity behind this decision, let’s envision a simple e-commerce site that we split into two domains:

  1. storefront
  2. admin panel

Splitting the site vertically makes perfect sense here, as the two domains are relatively self-contained.

Let’s imagine that after a few years the CEO wants to expand the company by allowing other sellers to offer their products through the site. This can be easily achieved by creating a new vertical slice for a partner’s portal domain.

A more complex change would be adding a new line of products to the store, which would require some changes from the current lineup. For this we can split the storefront into two new slices, i.e. books and perishable goods sections.

We can duplicate some code to ensure that the domains remain isolated but, as they grow, the cost of maintaining duplicate code can quickly outgrow its benefits.

To handle this, we can adopt horizontal slicing by defining domains based on user flows, i.e. the discovery process and purchase.

Each domain will include components that can exist on one or more pages and allow the related teams to own those user journeys.

In summary, vertical slices offer:

  1. Simpler technical implementation
  2. Pages are more cohesive, as the same team and product managers own them.
  3. Payload size per page tends to be smaller, especially when multiple frameworks are used.

On the other hand, horizontal slices offer:

  1. More freedom when slicing applications.
  2. Flows rather than pages tend to be more cohesive.
  3. Expanding the application is easier.

While there are strong technical benefits from using vertical slices, the decision needs to consider product needs and future growth.

Implementing vertical slices without clear independent domains becomes very hard to maintain, while the cost of horizontal slicing can easily exceed its short to medium value. This means that you have to choose which you want to implement carefully.

Incremental Upgrades?One of the main selling points for micro services and micro frontends architecture is the easier upgrade path you gain by managing multiple small applications that can be changed independently.

While this is a significant benefit, there are two additional considerations to discuss for micro frontends:

  1. Bundle size: grows exponentially for every new library we add. This affects load times which have a direct effect on the user journey.
  2. Shared components: it’s not trivial to share components between libraries, and while there are solutions, they impose limitations on your choices of libraries and development flows.

As a result, some companies chose to implement micro frontends using a single library such as React. While this might sound counterintuitive, it simplifies the adoption process, reduces bundle sizes, and can remain relevant for many years.

Micro Frontends ImplementationsiframesIframes are by far the simplest implementation for micro frontends, which ticks all the required boxes:

  1. Technology agnostic: allowing each application to use its own set of libraries.
  2. Self-contained: each application runs within its context, and each application can be built, tested, and deployed individually.
  3. Easy to implement: rolling out a bespoke solution is easy, and since each application provides its HTML file, the build process does not change radically.

Developers who wish to use an existing solution should take advantage of Luigi, an open-source library created by SAP, as it handles some of the more complex use cases such as cross iframe communication, 3rd party cookies, etc.

It’s worth noting that iframes work best for vertical slicing and that, despite the benefits, it has not gained wide traction in the community. As such, this option should be considered for smaller projects that need to get running quickly.

Module FederationModule federation is a relatively new feature in bundles, such as Webpack, that allows separate JavaScript applications to share and consume code at runtime.

It offers two exciting possibilities for developers:

  1. It can reduce bundle sizes by sharing common libraries, such as lodash, across multiple micro frontend applications. This is especially useful for the previous iframes implementation example.
  2. Companies that choose to forgo incremental upgrades and use a single library like React can use module federation to manage the loading of the different micro frontend applications.

A great Github resource demonstrates how to use Webpack module federation with different setups that provide a good starting point.

Module federation works best with single library projects that rely on vertical slicing. Horizontal slicing is possible but requires a more complex setup.

Single SPASingle spa is a popular library for managing more complex micro frontend applications.

It supports a sophisticated router called the Layout Engine that allows developers to easily combine multiple micro frontend applications that use different libraries within the same layout. This provides much better flexibility than standard vertical slices while avoiding much of the complexity of horizontal slicing. At the same time, it supports true stand-alone components called Parcels but advises limiting their use as they are rather complicated to manage. Single spa is a great solution for most complex applications that do not rely heavily on horizontal slicing.

Bit.devBit.dev is a tool for composing component-driven applications where a single component can be as small as a button or as big as an entire page. Bit.dev aims to fulfill the promise made by Web Components by allowing developers to freely combine components from different libraries into a single application while avoiding many of the hurdles that exist with Web Components. Unlike previous solutions, Bit requires a dedicated server where components are managed, organized into scopes, and shared across projects. It’s an exciting solution that is gaining popularity in companies like Tesla and Ebay but may not be ready for large enterprise-grade projects.

ConclusionMicro frontends can provide a valuable tool for organizations looking to scale their frontend applications. Adopting the right tool for the job, as with anything else, will have a lasting effect. Organizations should allocate time for technical investigation and align the technical domains with the product vision. Following the KISS design process is recommended when investigating technical solutions. Avoid selecting more complex solutions such as single spa and bit.dev that require deeper integration with the development process unless it’s clear that your organization will use its unique capabilities.

The post Starting With Microfrontends appeared first on Semaphore.

View Details

In today’s fast-paced business environment, organizations are constantly seeking ways to improve their operations and stay competitive. As hybrid DevOps teams—which combine in-house developers with external contractors or remote workers—become more popular, businesses want to make the most of their workforce.

However, managing and collaborating effectively with a hybrid team can be challenging. Despite the many benefits, managing a hybrid team requires a different set of collaboration tools and strategies than managing traditional office-based workforces, especially when it comes to fostering teamwork.

In this article, we will explore the latest team collaboration strategies for hybrid DevOps teams, including best practices for communication, project management, and more.

Why is it Important for DevOps to Collaborate? The technical aspects of DevOps teams are obviously important, whether it’s designing contact center software or effectively responding to issues in an IT system. But working in a team to achieve common goals is also a crucial tenet of any good DevOps organization.

When you hire engineers, it is important to consider how well they will work with the rest of the team. Effective collaboration is essential for the success of DevOps initiatives, as it allows teams to share information and resources, and to work together to improve processes and resolve problems. You could also follow, or even start, a DevOps blog to learn, collaborate, and share techniques. But what are the benefits of effective collaboration in a DevOps team?

  1. It sparks creativityWorking collaboratively is a great way to improve creativity. By bouncing ideas off each other, a team of engineers can find unique solutions that a single engineer might never have considered.

This can be invaluable in an industry that often requires creative solutions to difficult problems in order to have an advantage over competitors.

  1. It improves productivitySource: UnsplashIn hybrid work environments, boosting productivity can be one of the biggest challenges. Without some of the benefits of a focused office, sometimes employees can struggle to replicate productivity levels when working from home.

Encouraging collaboration can improve productivity by making engineers more engaged in their work while also making it more likely that they will arrive at solutions sooner than if they worked individually. During collaborative work, you may well find examples of emergent leadership, whereby employees begin to offer colleagues assistance and support to aid productivity.

  1. It provides better work transparencySimilarly, when a DevOps team works together, each team member can share information and stay up-to-date on the progress of work, ensuring that transparency is improved across the organization.

This helps to keep everyone on the same page and reduces the risk of confusion or delays due to lack of communication. It also helps to identify any issues or roadblocks early on and address them promptly, ultimately leading to a more efficient workflow.

  1. It reduces burnout Working hard on your own to try to solve problems regarding test automation, for instance, can sometimes be difficult and frustrating. While many engineers might enjoy working alone, it is well known that positive social interaction at work can help to reduce the chances of burnout.

This means that your best engineers will be more likely to continue performing at their highest levels and that you can retain your best talent.

  1. It’s time efficient By sharing knowledge and resources, DevOps teams can complete tasks faster and avoid duplication of effort. This is where asynchronous communication comes in. It allows hybrid teams to discuss problems efficiently and in a way that best suits them, while still working collaboratively. This is an especially useful method if your team is based in different time zones and arranging meetings can be difficult.

A more collaborative and productive workflow means that solutions will be produced earlier, saving your organization time and money.

These are only some of the benefits of effective collaboration. Overall, you may find that employees who collaborate, communicate, and create as a team will be happier—and that can only be a good thing. So, now that you understand the advantages, let’s explore how you can foster a collaborative culture.

8 Team Collaboration Best Practices for Hybrid DevOps Teams
The advantages of collaboration for hybrid DevOps teams are clear. It is a great way to improve productivity, reduce burnout and produce creative solutions. So how can you encourage effective collaboration in your DevOps team?

Source: Unsplash1. Take advantage of collaboration technologyOne of the advantages of the ubiquity of hybrid work is the amount of sophisticated collaboration technology that businesses can use, such as conference calling.

Really, there is technology to help any business, no matter where they’re based, communicate with teams all over the world. Unified communications software allows meetings to be conducted regardless of where the participants are located.

Through the use of video conferencing, instant messaging, and VoIP phone calls team members can stay in touch quickly and easily. This improves Dev team communication and encourages team collaboration.

More specifically, for DevOps teams, you can use project management tools to track progress and share files, while code management tools can be used to collaborate on code development.

It’s important to ensure that your team is effectively trained to use all of this technology efficiently. Once onboarded, collaboration technology is one of the best collaboration strategies for hybrid DevOps teams.

DevOps teams can also use a UX research repository to capture user emotions and needs in the most accurate way possible by analyzing user feedback interviews.

  1. Polish your plans and create a roadmap A key part of effectively collaborating in a hybrid DevOps team is having clear and well-defined plans and a roadmap. This means that goals and objectives should be clearly communicated and understood by all team members, and the roles and responsibilities within the DevOps team should be clearly outlined.

Additionally, a detailed project plan should be created, which outlines the steps that you need to achieve to meet the project’s goals. This plan should be regularly reviewed and updated. Having a clear roadmap in place will help to keep everyone on the same page and ensure that everyone is working towards the same goals.

  1. Create a communication plan Source: UnsplashWhen designing collaboration strategies in a DevOps project, it can be helpful to create a communication plan. This should outline the different channels and methods of communication that engineers will use.

For example, you can schedule regular virtual meetings to discuss progress and resolve any issues, while instant messaging and email can be used for quick, informal communication.

It’s also important to establish clear lines of communication between different teams and ensure that everyone knows who to contact in case of an emergency. If your team is designing acceptance development programs, for instance, you should outline channels of communication with the final users of those programs.

  1. Create an inclusive hybrid environmentAny good project management strategy should ensure that all team members feel included and valued. All of your employees should be encouraged to share their thoughts and ideas, and everyone’s contributions should be acknowledged.

In virtual meetings, you could use a shared document for engineers to suggest ideas if they don’t feel comfortable speaking up in front of a large group. A healthy environment that is achieved through open and transparent communication will ultimately lead to better results.

Remember to be as inclusive as possible so all members of your team can participate. Consider adding closed captions for those with hearing impairments and provide clear transcriptions after each meeting. For those with visual impairments ensure that each member identifies themselves when speaking and any images used are visually described.

  1. Identify challenges You will encounter problems when designing collaboration strategies for your hybrid DevOps team. Time-zone differences or software availability can make collaboration difficult.

However, you can minimize these challenges by identifying them early. Discuss them with your team members and log information such as time zones. This means that you will be able to recognize potential issues before they cause any damage. Using a business phone system to allow your team to stay connected can help with remote work.

  1. Build a hybrid work policy Having a clear hybrid work policy can ensure that everyone is on the same page when it comes to hybrid collaboration. This policy should be designed collaboratively, building buy-in among team members.

Your policy should define the types of work arrangements that will be supported. You should outline the guidelines and expectations for working remotely, as well as communication channels, technology requirements, security protocols, and expectations for team members.

By designing a clear hybrid work policy, you can make sure that your DevOps team can collaborate in a harmonious environment where each team member understands their responsibilities.

  1. Use automationSource: UnsplashAutomation is making all digital work easier, from CI/CD to security access requests. Aspects of DevOps collaboration can also be automated, helping to streamline repetitive and time-consuming tasks.

Automating the testing of code, for instance, can free up more time for the sort of creative work in which collaboration is crucial, allowing team members to focus on more important and strategic work.

  1. Encourage diversity of ideasEffective collaborative work should ensure that team members are encouraged to share their unique perspectives and ideas. This helps to foster an environment of creativity and experimentation. You should try to create an open and inclusive environment where everyone feels comfortable sharing their thoughts and ideas, which will also serve to remove the potential for any discrimination in the workplace–though this is not without work and acknowledgement from the top.

For instance, a team that is trying to prevent spam emails could be divided into small groups in a virtual meeting to brainstorm responses to the question ‘how can we stop spam emails?’ This can prevent the most confident speakers from dominating group discussions and ensure that more diverse voices are heard.

Additionally, team members should be encouraged to participate in cross-functional teams, which can help to bring different perspectives to the table. By encouraging diversity of ideas, teams can work together more effectively and achieve better results.

Collaboration – the key to a successful hybrid DevOps teamMany DevOps teams are moving permanently to a hybrid working environment. Despite the importance of collaboration, managing a hybrid team’s collaboration can pose unique challenges. This article provided you with the strategies that can help you to overcome these issues.

From using technology to creating inclusive environments, these practices can lead to improved creativity, productivity, and efficiency, while also reducing burnout. By implementing these strategies in your organization, you can make the most out of your hybrid team.

The post Team Collaboration Strategies for Hybrid DevOps Teams appeared first on Semaphore.

View Details

Large-scale JavaScript projects are likely to have multiple frontends and backends that interact and share libraries and interfaces among them. This means that there is a need for effective collaboration amongst the different teams working on these applications.

Nx is a tool used at Google to scale multiple applications used by different developer teams within a single monorepo. In this post, we’ll learn about Nx with a practical example of building a JavaScript Monorepo. You can learn more about Nx from its source documentation here. You can also try out the example project in this tutorial here.

Why do you need a Monorepo for your JavaScript application?A full-stack JavaScript project at scale can have multiple frontends running on different frameworks. Each frontend could interact with different backend applications and each backend application might further need to interact with the others. However, each independent application may have a ton of similarities because they all belong to the same JavaScript ecosystem.

In these cases, monorepos become a great way to simplify sharing code, tests and interfaces between various applications. Monorepos have a common code base or git repository that manages the different applications. They help strengthen collaboration amongst different teams since everyone has more visibility of the project, as it is a single large codebase rather than multiple fragmented codebases.

Tech giants like Microsoft and Facebook also use monorepos for many of their JavaScript projects, so it’s a proven approach

What is Nx?A monorepo is merely an approach to developing and deploying large-scale projects. However, to use monorepos effectively, you need the right tooling to manage them.

Nx is a tool that assists in testing, building and deploying large JavaScript apps at scale. Let’s say you need to scale a JavaScript project that has two separate frontends running on ReactJS and Angular. You also have a JavaScript backend on NodeJS or NestJs that needs to communicate with these two frontends. You need to share libraries and interfaces between these three applications–let’s take a look at how Nx can help us do that.

Creating an Nx WorkspaceAn Nx project in its bare bones is termed a workspace. You don’t need to install Nx separately in order to create an Nx project, you can do it directly using npx, which comes by default when you install NodeJS and NPM. So, let’s dive right into it and create a new Nx workspace using the following command:

npx create-nx-workspace Once you run the command, you’ll be prompted to download the Nx pkg (if not already installed) and then you get to start specifying some common details about your workspace. We’ll choose the integrated workspace option. Also, we’ll use CSS as the default stylesheet.

You should now have a new Nx workspace created. Sweet! Let’s take a quick peek inside the boilerplate project we have currently:

Notice that the project itself comes pre-configured with Jest and ESLint. You can find the relevant config files (jest.config.ts and .eslintrc.json) in the root directory.

Further, there is a package.json file at the root of the project. Since we’re working with a monorepo, we’ll have a single dependency management for all of our applications inside our Nx workspace. There is also an nx.json file which specifies the config of your Nx workspace.

There are two important directories in the workspace – the apps and the libs directories. The apps directory will store all the various applications of your project. If you expand the apps directory, you’ll find an angular project with end-to-end testing enabled. If we add more angular or react projects in our workspace, those projects will go inside this apps directory.

The libs directory will store all the code that you wish to share amongst different applications in your monorepo. If you’re using TypeScript, the libs directory is a good place to put some commonly shared interfaces. You can also put some common UI elements here.

The Nx ConsoleWe can use a powerful VS Code extension called Nx console that helps generate Nx commands easily. You can find the extension here. Let’s go ahead and install this:

Adding Angular to the Nx WorkspaceLet’s now add a new Angular app to our Nx workspace. We can do this directly using the Nx console, so you don’t have to remember any specific commands as such. Head over to the Nx console and select the generate option from the “GENERATE & RUN TARGET” section. That should drop a bunch of commands. You can then select the “Create an Angular application” command:

Once you do that, you should get a form specifying some options for your Angular project. As you start selecting these options, you can see the actual command that is generated on the fly.

Let’s give our Angular application a name and set css as the default style:

I’ve also added TailwindCSS to be configured by default. If you use TailwindCSS, this can save you quite a bit of time later, as it does a lot of configuration for you.

Adding Test tools and ESLintIn the same workflow, we can configure Cypress and Jest to be added to our Angular application. Under the e2eTestRunnersection, choose Cypress from the dropdown menu. You can also add ESLint to the project in the linter section.

Next, scroll down to the unitTestRunner section. Select Jest from the dropdown menu:

And that’s it! If you hit the Run button, Nx should automatically generate an Angular project configured with Jest, Cypress and ESLint. Under the hood, in this case, it ran the following command:

npx nx generate @nrwl/angular:application angular-monorepo --addTailwind --no-interactive Adding React to the Nx WorkspaceNow that we’ve added an Angular application to our monorepo workspace, let’s add another frontend to our monorepo. This time, we’ll add a React application to our Nx Workspace. In order to have React support inside your Nx console, you’ll need to install the @nrwl/react library.

Run the following command inside the root of your project:

npm i @nrwl/react Notice that this library is installed in the node_modules folder under the root directory of your Nx workspace. This is where all your libraries will live. Now, we can go back to the Nx console and see a list of commands available to generate a React project:

Once you do that, the next steps should be very similar to what we did in the previous section for generating an Angular application. For our React application, we’ll also choose to “Generate application with routes” in the Routing section of the Nx generate config.

Once you click run, Nx should run the following command to generate a React application inside your Nx workspace:

npx nx generate @nrwl/react:application react-monorepo --routing --no-interactive And now we should have a React project generated inside the apps directory:

Adding a Backend application to the Nx WorkspaceSimilar to the previous section, we can also add a NodeJS application to our Nx workspace. First, we’ll add the @nrwl/node dependency to the project:

npm i @nrwl/node Now, we can create a new NodeJS application by running:

nx g @nrwl/node:app node-app I chose Express as the default framework with Node for this, but you can also choose NestJS or Fastify. Nx provides support for all the popular NodeJS frameworks.

You should now have a NodeJS with Express project inside your Nx workspace:

Awesome! Now that we have all our applications generated in our monorepo, let’s try running them.

Running Nx Projects LocallyHead over to the Nx console, chose the run command and then select the individual projects you want to run:

Once you run each of these individual commands, Nx should kickstart the two frontend projects for you, here’s what the Angular project should look like:

Similarly, you should also have your React Project up and running:

In the same way, we can also run the NodeJS project locally.

Sharing Libraries with NxOne of the most interesting features of Nx for developing a monorepo is sharing libraries. In fact, abstraction becomes even more important in a monorepo because you can share libraries and interfaces between multiple apps in the same codebase.

Creating a Shared LibraryLet’s see how we can use Nx to generate and consume a shared library between all three applications inside our project. We can create boilerplate for a new library in the Nx console.

Once you do that, you should get a similar form to create a new library. For brevity, we’ll just create a vanilla Typescript library. However, you can also create framework-specific libraries. Nx also provides support for adding and specifying tags to your libraries. Tags give special meaning to a library and can be used to indicate its purpose and scope.

Once you run the command, Nx will actually run the command shown below to create a new library called utils inside the libs directory:

npx nx generate @nrwl/js:library utils --no-interactive If you expand the src folder, you’ll notice there’s a utils.ts file inside the lib directory. This is where we can write the code for our library.

I’m going to go ahead and create a simple variable that we’ll then export:

const name='fuzzy';export default{ name} Then, I’ll update the entry point of our library called index.ts inside the src directory:

import utils from './lib/utils';export default utils; Since the entry point file is what actually gets exported, I simply import the library here and export it as default.

Using the Shared LibraryOur newly-generated shared library called utils can be used by both frontend applications. However, in order to do that, we need to make a change in the tsconfig.json file of both projects.

Under the compilerOptions , we need to specify a paths property where all our custom paths relative to the root directory will live. Any applications using this relative path must have this specified in their tsconfig.json files. Inside the tsconfig.json of both the React and Angular app, add the following paths property:

{ "compilerOptions": { ... "paths": { "@js-monorepo/utils": ["libs/utils/src/index.ts"] } }, ...} Now, you can simply import the library using the @ declarative followed by the name of the project and then the library name. Let’s use it inside our Angular application. Inside the apps/angular-monorepo/src/app/app.component.ts file, we can import the library and use the name exported variable from it, as shown below:

import { Component } from '@angular/core';import utils from '@js-monorepo/utils';@Component({ selector: 'js-monorepo-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'],})export class AppComponent { title = utils.name;} It’s just like using a regular node module dependency in a JavaScript application.

I’ve also updated the app.component.html file as shown below:

```

Nx Angular Monorepo

Welcome {{title}} `` Great! Let’s do the same for our React app inside theapps/react-monorepo/src/app/app.tsx` file:

// eslint-disable-next-line @typescript-eslint/no-unused-varsimport styles from './app.module.css';import utils from '@js-monorepo/utils';import { Route, Routes, Link } from 'react-router-dom';const name=utils.nameexport function App() { return ( <> <h1>Nx React Monorepo</h1> Welcome {name} <div /> {/* START: routes */} {/* These routes and navigation have been generated for you */} {/* Feel free to move and update them to fit your needs */} <br /> <hr /> <br /> <div role="navigation"> <ul> <li> <Link to="/">Home</Link> </li> <li> <Link to="/page-2">Page 2</Link> </li> </ul> </div> <Routes> <Route path="/" element={ <div> This is the generated root route.{' '} <Link to="/page-2">Click here for page 2.</Link> </div> } /> <Route path="/page-2" element={ <div> <Link to="/">Click here to go back to root page.</Link> </div> } /> </Routes> {/* END: routes */} );}export default App; Awesome! Let’s go ahead and see how our applications look now:

As you can see, both our apps are able to consume the shared library. We can do the same for our NodeJS application. Remember to update the tsconfig.json there as well by appending the shared lib “paths” to the “compilerOptions“. Then, update the apps/node-app/src/main.ts file as shown below:

import express from 'express';import utils from '@js-monorepo/utils';const port = process.env.PORT ? Number(process.env.PORT) : 3000;const app = express();app.get('/', (req, res) => { res.send({ message: 'Hello NodeJS API' + " " + utils.name });});app.listen(port, () => { console.log(`[ ready ] http://localhost:${port}`);}); And now you’ve used the same shared library in your backend application:

How Nx optimizes application buildsWe’ve seen that monorepos can be extremely useful, but building and testing an entire monorepo can be slow and have performance implications. Therefore your monorepo tool needs to optimize the build process so you can test and generate builds faster.

Luckily, Nx automatically detects changes in your code and helps to test and build all your apps in isolation. This means that if only one of your application’s source code changes, Nx will only rebuild that application. Let’s see how we can build our entire monorepo using Nx now.

Head back to the Nx console, choose the build option and select the build all command. Alternatively, you can also run the build command directly:

npx nx run-many --target=build Once you do that, you should see Nx building all your applications one by one.

At the time of running this build, I had already built the library, so you can see that Nx skipped rebuilding it since nothing changed in that project.

ConclusionNx is a great tool for scaling JavaScript monorepos with multiple frontends and backends. You can extend the idea of shared libraries and further share common typescript interfaces between your applications. For instance, you could share the API schema between your backend and frontend. Until next time!

The post Scaling a Javascript Monorepo With Nx appeared first on Semaphore.

View Details

Diversity in a software team refers to the variety of characteristics, experiences, and backgrounds of team members, including but not limited to race, gender, age, religion and cultural background. A team with diverse perspectives and experiences can lead to a variety of ideas and approaches to problem solving, resulting in more creative and innovative solutions. Diverse teams are more likely to consider a wider range of viewpoints and information, leading to better decision-making. Teams with diverse members are also more likely to have a better understanding of and empathy for the needs and perspectives of different users and customers. A diverse team can help ensure that the products and services developed better represent and cater to the needs of a diverse user base. Teams that are inclusive and respectful of diverse backgrounds and perspectives tend to have more positive team dynamics, with improved communication and collaboration among team members.

In a team made up of individuals with diverse backgrounds, there is a wider range of ideas and approaches to solving problems, leading to more creative and innovative solutions. For example, a team member with a background in psychology might approach a problem differently than a team member with a background in computer science, resulting in a unique solution that leverages both perspectives. This can lead to increased efficiency and improved results.

Diversity promotes equality and inclusiveness, creating a workplace where everyone feels valued and respected, regardless of their background. This can improve employee satisfaction, motivation, and overall job performance, leading to a more productive and successful team and organization.

What does a diverse software team look like?A diverse software team could be made up of individuals with a variety of personal characteristics, experiences, and backgrounds, including but not limited to:

  • Gender: team members with a mix of different gender identities and expressions.
  • Race and ethnicity: team members representing a variety of racial and ethnic backgrounds.
  • Age: a mix of team members representing different age ranges and life stages.
  • Sexual orientation: team members who identify as heterosexual, gay, lesbian, bisexual, or any other sexual orientation.
  • Religion: team members with different religious beliefs and practices.
  • Educational background: team members with a variety of educational backgrounds, including computer science, psychology, mathematics, and liberal arts.
  • Cultural background: team members with different cultural backgrounds, including different countries of origin, mother tongues, and cultural traditions.

Such a team would have a mix of different perspectives, experiences, and skills, leading to a more creative and innovative work environment, improved decision-making and better representation of diverse populations. By fostering an inclusive and respectful workplace, this team would be more productive and effective in achieving its goals.

How diversity supports product developmentA specific real-life scenario where diversity in a software team helped improve quality was during the development of the assistive technology software “Voice Recognition for the Visually Impaired.” The development team included individuals with diverse backgrounds, including people with visual impairments and those with expertise in assistive technology. This diversity in perspective and life experience helped the team to better understand the needs of visually impaired users and to create software that was more user-friendly and effective.

For example, team members who were visually impaired brought their own experiences and provided insights into the difficulties they faced in using technology. This helped the team to identify areas where the software needed improvement and to implement changes that made it more accessible and effective. The result was a software product that was well-received by users and made a positive impact in their daily lives. This scenario highlights how diversity in a software team can lead to better understanding of user needs and result in higher-quality products.

Steps to embrace diversity in your team1. Start with self-reflectionStarting with self-reflection is important to embrace diversity in a software team because it helps individuals to recognize and address their own unconscious biases and assumptions. This is a crucial step in creating an inclusive environment where everyone feels valued and respected.

When team members engage in self-reflection, they can identify their own biases and work to overcome them. For example, a team member may realize that they have a tendency to discount the ideas of individuals who are different from themselves, such as people from different cultures or with different life experiences. This awareness can help the team member to actively seek out diverse perspectives and to be more open to new ideas.

By starting with self-reflection, individuals can create a foundation for more diverse and inclusive teams. When team members understand their own biases, they can work together to create a supportive and respectful environment where everyone feels heard and valued. This can lead to better collaboration, more innovative ideas, and, ultimately, higher-quality software products.

2. Develop a diversity and inclusion policyDeveloping a diversity and inclusion policy is important to embrace diversity in a team because it sets clear expectations for behavior and helps to create a culture of inclusiveness. A well-designed diversity and inclusion policy can serve as a roadmap for creating a more diverse and inclusive workplace and can help to ensure that everyone on the team feels valued and respected.

Having a written policy in place can help to ensure that everyone is on the same page when it comes to creating a supportive and inclusive environment. It can also provide guidance on how to address any issues that may arise, such as discrimination or unequal treatment. This can help to create a sense of accountability and can encourage everyone to work together to promote diversity and inclusion in the workplace.

A well-designed diversity and inclusion policy can also help to create a more attractive work environment for a diverse range of individuals. When potential team members see that a company is committed to diversity and inclusion, it can help to attract a more diverse group of applicants and can also help to retain current employees who feel valued and respected.

3. Encourage a culture of respectEncouraging a culture of respect in a diverse team means creating a work environment where all team members feel valued, heard, and respected. This respect should be given regardless of their personal characteristics, experiences, and backgrounds. This involves fostering open and honest communication, active listening, and a willingness to understand and respect different perspectives and experiences. This may include providing training on how to avoid discrimination and bias to team members.

4. Foster a culture of continuous learningEncourage employees to learn about different cultures and backgrounds, and provide opportunities for them to do so, such as attending diversity workshops or participating in cultural activities. A culture of continuous learning creates an environment where individuals are open to new ideas and perspectives, which can be beneficial in promoting diversity and inclusion.

Continuous learning helps individuals stay current with new knowledge, skills, and practices, which can be important in running a diverse team by ensuring that team members are up-to-date with best practices and new ways of thinking. Empathy towards others develops by learning about different cultures and perspectives. This creates a sense of belonging within a team, which can be very beneficial while working together .

5. Make sure everyone is heardMake sure that everyone has an opportunity to share their opinions and ideas, and that their contributions are valued and respected. This can be achieved and implemented within a team in several ways:

  • Establish ground rules for communication: develop clear guidelines for how team members should communicate with each other and ensure that everyone understands and adheres to these guidelines.
  • Encourage active listening: encourage team members to actively listen to each other and avoid interrupting or talking over one another.
  • Create opportunities for participation: provide opportunities for every team member to contribute, such as through rotating leadership roles, regular check-ins, or brainstorming sessions.
  • Provide diverse channels for feedback: allow team members to provide feedback in different ways, such as in writing, through one-on-one conversations, or in group meetings, to ensure that everyone feels comfortable sharing their thoughts.
  • Emphasize respect for different perspectives: encourage team members to respect and value different perspectives, even if they don’t necessarily agree with them.
  • Address unconscious biases: take steps to address any unconscious biases that may be preventing some team members from being heard, such as by providing training or coaching.

By implementing these strategies, teams can create an environment that values and encourages input from everyone, leading to better collaboration and more innovative solutions.

6. Celebrate diversityCelebrating diversity in a team can help create a positive and inclusive work environment that values and respects everyone’s unique perspectives and contributions. Here are some ways to celebrate diversity in a team:

  • Organize cultural events: plan events that celebrate the various cultures and traditions represented in the team, such as food festivals, music concerts, or holiday celebrations.
  • Create a diversity and inclusion task force: establish a team or committee that is focused on promoting diversity and inclusion within the workplace, and provide them with resources and support to organize activities and events.
  • Offer training and development opportunities: provide team members with training and development opportunities that focus on diversity, equity, and inclusion, and highlight the importance of embracing differences in the workplace.
  • Recognize and highlight diverse perspectives: acknowledge the unique perspectives and contributions of each team member, and create opportunities for individuals to share their stories and experiences.
  • Incorporate diversity into team-building activities: use team-building activities that are designed to promote diversity and inclusion, such as team-building exercises that encourage collaboration and teamwork across different backgrounds and experiences.
  • Foster open communication: encourage open and honest communication within the team, and create a safe and supportive environment where team members feel comfortable sharing their thoughts and ideas.

7. Hold yourself and others accountableHolding yourself and others accountable for upholding commitments to diversity is critical for several reasons:

  • Ensuring progress: accountability helps to ensure that commitments to diversity are being met and that progress is being made. Without accountability, commitments can easily fall by the wayside or be overlooked, and progress can stagnate.
  • Demonstrating commitment: when individuals and teams hold themselves and others accountable for upholding commitments to diversity, it demonstrates a strong commitment to diversity and inclusion. This sends a message to others that diversity is a priority and that efforts to promote it are taken seriously.
  • Encouraging continuous improvement: holding yourself and others accountable for upholding commitments to diversity helps to encourage continuous improvement. When individuals and teams regularly assess their progress and hold themselves accountable for meeting their goals, they are more likely to identify areas for improvement and take action to address them.
  • Fostering trust: when individuals and teams hold themselves and others accountable for upholding commitments to diversity, it fosters trust and respect within the team. This helps to create a safe and supportive work environment where everyone feels valued and included.

Overall, holding yourself and others accountable for upholding commitments to diversity is essential for promoting a diverse and inclusive workplace culture. To hold yourself accountable, you must first understand the importance of diversity and actively seek to educate yourself on different cultures, perspectives, and experiences. This includes examining your own biases and recognizing areas where you may need to improve. It also involves taking actions to promote diversity and inclusion, such as seeking out diverse perspectives and experiences, advocating for underrepresented groups, and being an ally to those who face discrimination.

As an example, imagine that hiring managers in a company took the commitment of promoting diverse workplace culture. They educated themselves on issues related to diversity and inclusion. They then sought feedback from colleagues and employees from underrepresented groups on how the company could improve its hiring practices. Based on this feedback, they implemented several changes to the company’s hiring process. For example, they worked with HR to remove any unnecessary requirements that might have been excluding qualified candidates from underrepresented groups. They also made sure that the interview panels were diverse and that everyone involved in the hiring process received training on reducing unconscious bias.

To hold others accountable, you (and other all members of the team) must be willing to speak up when you see actions or behaviors that do not align with DEI (Diversity, Equity, and Inclusion) values. This may involve calling out discriminatory language or behaviors, challenging systems and policies that perpetuate inequality, and holding individuals and organizations responsible for making changes that promote equity and inclusion.

Ultimately, holding yourself and others accountable for upholding commitments to diversity means actively working towards creating a more equitable and inclusive world, both in your personal and professional life. It involves recognizing the value of diversity and actively working to promote it, while also holding yourself and others responsible for ensuring that these values are upheld.

What should we do with members who refuse to open up to diversity?Letting go of team members who refuse to open up to diversity should always be a last resort after other methods have been attempted and have failed. Here are some steps to take before making the decision to let someone go:

  • Have a conversation: have a one-on-one conversation with the team member in question. Explain your concerns and give them an opportunity to express their perspective. Be clear about the behavior that needs to change and what the consequences will be if it does not.
  • Offer support: provide support to team members who may be struggling to adapt to a more diverse team. This could include coaching or mentoring, or connecting them with resources to help them learn and grow.
  • Document everything: document all conversations, incidents, and actions related to the team member in question. This will help you make an informed decision if you ultimately need to let them go.

If, after taking these steps, the team member still refuses to open up to diversity, it may be necessary to let them go. However, it is important to do so in a way that is respectful and professional. Provide clear reasons for the decision and offer any support or resources that may help the team member find success elsewhere.

ConclusionDiversity in teams can bring many benefits, including improved problem-solving, creativity, and innovation. A software development team may have members from different backgrounds and experiences, with varying levels of expertise in different areas. When these team members work together on a project, they bring different perspectives, insights, and knowledge that can help improve the quality of the software product. A team with diverse cultural backgrounds and experiences may bring unique perspectives on user experience design that can lead to a more user-friendly and accessible product. Similarly, a team with different technical backgrounds may be able to identify and solve technical challenges in different ways, leading to more robust and efficient code.

In addition, a diverse team can help ensure that the software product meets the needs of a broader range of users. For example, a team with members who have disabilities may be more likely to identify and address accessibility issues in the software, making it easier for people with disabilities to use. A team with members from different regions may be more likely to identify and address cultural and language differences in the software, making it more inclusive and accessible to people from different parts of the world.

In summary, diversity in software teams can bring a range of perspectives, insights, and knowledge that can help build better software products. By working together, team members can identify and address issues that may have been overlooked, resulting in a more inclusive, accessible, and effective software product.

The post Embracing Diversity in a Software Team: Why It Matters and How to Do It appeared first on Semaphore.

View Details

As with any other technology, React applications can involve errors. By default, React responds to a fatal error with a blank page. This is not ideal, because users have no way to understand what happened and why. This is a problem for developers because poor error handling affects user experience and the reputation of your application. Fortunately, you can fix this by adding an error handling layer to your React application.

An error handling layer allows you to catch errors and display clear and helpful error messages to the user. This helps you avoid undesired crashes and improve the overall user experience as a result. Centralizing all error handling logic in one place provides several benefits and mitigates the problem mentioned earlier. This is what a React error handling layer is all about!

Let’s learn what an error handling layer is, understand why your React app should have one, and see two approaches to building an error handling layer in React.

What is an Error Handling Layer in React?In React, an error handling layer is the part of your frontend architecture that is responsible for detecting and handling errors that occur in your application. In other terms, it encapsulates and centralizes error handling logic in one place.

A React error handling layer generally takes care of:

  • Error detection: the layer intercepts errors that occur during the execution of the frontend application.
  • Error handling: after detecting an error, the layer handles it gracefully. In particular, it prevents the error from crashing your application and presents a custom error page to users instead. Behind the scenes, the layer can also log the error or report it to an error monitoring service.

By adding an error management layer to your React architecture, you can ensure that errors occurring in your application will be handled effectively and consistently. These are just some of the benefits that an error management layer can bring to your application. Read on to learn more.

Why should your React App have an Error Handling Layer?As mentioned earlier, React has no built-in mechanism to recover from errors happening during rendering. When an error occurs, the only option React has is to unmount the entire application. As a result, users will see a blank screen. This is how React handles errors by default and, as you can imagine, this is far from an ideal solution.

With an error handling layer, you can override the default behavior and avoid this. In detail, an error handling layer allows you to:

  • Avoid crashes: in case of an error, the layer intercepts it and allows you to handle the error as you want. This means preventing your application from crashing due to unhandled errors.
  • Improve user experience: whenever the layer detects an error, it displays a nice error page to the end user. Users tend to prefer using an application that provides useful error messages to one that crashes.
  • Centralize error handling logic: the layer stores the entire error handling logic of the application in the same place. This makes it easier to deal with errors consistently and integrate error logging or monitoring into your application. You can then use this collected information to identify the root cause of errors, debug, and fix them.

These are all good reasons why your React app should have an error handling layer. Keep reading and see two approaches that you can follow to add an error handling layer to your frontend React architecture.

Error Handling Layer with Error Boundaries in ReactError boundaries have been the official way to handle errors since React 16, which was released in 2017. Specifically, error boundaries are React components that can catch JavaScript errors occurring anywhere in their child component tree. They allow you to display a fallback UI component when an error occurs, which handles the error gracefully and avoids a fatal crash.

Note that standard error boundaries catch errors only during rendering, in lifecycle methods, and in component constructors. Thus, if a component within these boundaries throws an error, the error boundary will intercept it. Otherwise, it will ignore the error.

Let’s now find out how to build and use an error boundary in React to implement an error handling layer!

Creating an Error Boundary ComponentTo create an error boundary in React, you need to define a class component that involves:

  • A state variable hasError: used for determining whether the error boundary has intercepted an error.
  • A static method called getDerivedStateFromError(): a React lifecycle method that is invoked after a descendant component throws an error. The value returned by the method updates the state of the error boundary component.
  • A componentDidCatch() method: a special React lifecycle method that is called when the error boundary catches an error. You can use it for logging the error or reporting it to an application performance management system.
  • A render() method with if-else logic`: in case of an error, it returns the fallback UI component. Otherwise, it displays the child components wrapped by the error boundary.

Note that React supports the key lifecycle methods getDerivedStateFromError() and componentDidCatch() only on class components. This means that an error boundary cannot be a functional component.

Here is what an error boundary class component should look like:

import React from "react";import ErrorPage from "./ErrorPage";export default class StandardErrorBoundary extends React.Component { constructor(props) { super(props); // to keep track of when an error occurs // and the error itself this.state = { hasError: false, error: undefined }; } // update the component state when an error occurs static getDerivedStateFromError(error) { // specify that the error boundary has caught an error return { hasError: true, error: error }; } // defines what to do when an error gets caught componentDidCatch(error, errorInfo) { // log the error console.log("Error caught!"); console.error(error); console.error(errorInfo); // record the error in an APM tool... } render() { // if an error occurred if (this.state.hasError) { return <ErrorPage />; } else { // default behavior return this.props.children; } }} When an error occurs in any of its child components, StandardErrorBoundary intercepts it, logs it, sets hasError to true, and renders the <ErrorPage /> fallback UI component.

This is what ErrorPage might look like:

export default function ErrorPage(props) { return ( <div className={"error-page"}> <div className={"oops"}>Oops!</div> <div className={"message"}>Something went wrong...</div> </div> );} You can customize and style your fallback UI component as you like. An effective ErrorPage component should inform users of what happened, without going into too much detail or worrying them.

You now know how to define an error bounding component in React! All that remains is to see how to use it.

Using Your Error BoundaryTo implement an error handling layer in React, simply wrap your top-level app component with StandardErrorBoundary:

<StandardErrorBoundary> <MyApp /></StandardErrorBoundary> From now on, any React-related error occurring in the app will be intercepted and handled by the error boundary.

Keep in mind that a React app can have multiple error boundaries. Each of them will take care of handling errors in different component subtrees. However, to centralize the error logic in one place, you should use only one top-level error boundary.

Limitations of this approachError boundaries are a powerful tool but also come with several limitations you should take into account. Let’s dig into them.

As mentioned in the official documentation, error boundaries cannot catch errors in:

  • Asynchronous code
  • Event handlers
  • Server components
  • The error boundary itself

In other words, an error boundary can only handle errors thrown during React’s lifecycles.

Also, error boundaries must be class components and do not support hooks.

These limitations can easily pose a problem for your React application, especially considering that JavaScript relies heavily on event callbacks and async code. This is why we need to explore a different approach to error handling in modern React applications.

Error Handling Layer with react-error-boundaryreact-error-boundary is an npm React library that provides an easy and reliable way to handle errors with error boundaries. It simplifies the process of creating error boundaries and offers effective solutions to overcome the limitations of a standard error boundary.

Simply put, react-error-boundary greatly simplifies error handling in React. Add it to your project’s dependencies with:

npm install react-error-boundary Next, keep following the tutorial and learn how to build an error handling layer in React with react-error-boundary.

Getting started with the ErrorBoundary Componentreact-error-boundary exposes an ErrorBoundary component, which supports several props to help you build an error boundary component with no effort.This is what an error boundary created with react-error-boundary may look like:

import ErrorPage from "./ErrorPage";import { ErrorBoundary } from "react-error-boundary";export default function ReactErrorBoundary(props) { return ( <ErrorBoundary FallbackComponent={ErrorPage} onError={(error, errorInfo) => { // log the errorconsole.log("Error caught!"); console.error(error); console.error(errorInfo);// record the error in an APM tool... }} > {props.children} </ErrorBoundary> );} The FallbackComponent prop contains the fallback UI component to render in the event of an error. When this happens, ErrorBoundary invokes the callback function passed to onError. Otherwise, if no error occurs, ErrorBoundary renders the child components that it wraps. In other terms, this snippet does exactly what the error boundary defined previously does, but with less and more readable code.

Just as we did earlier, wrap your React top-level component with ReactErrorBoundary to centralize the error handling logic:

<ReactErrorBoundary> <MyApp /></ReactErrorBoundary> Do not forget that an error boundary component built with react-error-boundary can be either a functional or a class component. You are no longer limited to class components, and it is up to you to make the best decision according to your goals and preferences.

Resetting your app after an Errorreact-error-boundary also provides a way to recover from errors caught by the error boundary component. Specifically, the onReset prop accepts a callback function that ErrorBoundary automatically passes to the FallbackComponent.

Update your ReactErrorBoundary component as shown below:

import ErrorPage from "./ErrorPage";import { ErrorBoundary } from "react-error-boundary";export default function ReactErrorBoundary(props) { return ( <ErrorBoundary FallbackComponent={ErrorPage} onError={(error, errorInfo) => { // log the errorconsole.log("Error caught!"); console.error(error); console.error(errorInfo); }} onReset={() => { // reloading the page to restore the initial state // of the current page console.log("reloading the page..."); window.location.reload(); // other reset logic... }} > {props.children} </ErrorBoundary> );} What the onReset callback function does is to simply reload the current page via JavaScript. This is the most basic error reset logic possible. In more complex applications, you may need to restore the state of the app to a previous point or update the Redux state.

By default, ErrorBoundary passes to the FallbackComponent the following two props:

  • error: stores the error object caught by the error boundary.
  • resetErrorBoundary: contains the callback function passed to the onReset prop, if defined.

So, you can now add a retry button to the ErrorPage as shown below:

import "./ErrorPage.css";export default function ErrorPage(props) { return ( <div className={"error-page"}> <div className={"oops"}>Oops!</div> <div className={"message"}>Something went wrong...</div> {props.resetErrorBoundary && ( <div> <button className={"retry-button"} onClick={props.resetErrorBoundary}> 🔄 Try Again! </button> </div> )} </div> );} The fallback page shown in case of an error will now have a “Try Again!” button. When the user clicks on it, the onReset function will be executed and the current page reloaded. If the error is not in the initial rendering, but arose as a result of a particular interaction, the application will return to working as expected.

Great! You can now recover from errors!

Handling Errors in Async CodeAnother great feature offered by react-error-boundary is the useErrorHandler() hook. This allows you to catch errors that would not otherwise be caught by a traditional React error boundary. This means that you can take advantage of useErrorHandler() to intercept errors during API requests, event handlers, and more.

Suppose that one of the pages in your React application relies on a crucial API call. When this API fails, you want to raise an error and handle it in the error layer. Since API calls involve asynchronous code, you cannot do that with a standard error boundary. Thanks to useErrorhandler(), you can achieve the desired result with:

import { useEffect } from "react";import { useErrorHandler } from "react-error-boundary";export default function MyPageComponent(props) { const handleError = useErrorHandler(); useEffect(() => { // API call fetchSomeData().then(() => { // ... }).catch((e) => { // propagate the error to the error boundary handleError(e); }) }, []) // return ...} Similarly, you can adopt useErrorhandler() in other situations where standard error boundaries fall short. Also, keep in mind that useErrorhandler() propagates the error to any error boundary, regardless of whether it is implemented with react-error-boundary or with the standard approach.

Et voilà! Your React error handling layer is more powerful than ever!

Error Handling Layer in actionYou can play with the live demo React application below to see how the two approaches behave differently:

When you disable the error handling layer, the first button will result in a blank page. Do not forget that this is how React natively handles errors. On the other hand, the async error will be logged but nothing more will happen.

If you use the error handling layer with a standard error boundary, the first button will lead to the error page. Yet, the async error will not be intercepted as desired.

With the react-error-boundary error layer enabled, both sync and async errors will be intercepted and handled as desired. Also, when clicking the “Try Again!” button, the page will reload and the application will recover from the error.

If you want to explore the code of the demo application or test it locally, clone the repository that supports this article with:

git clone "https://github.com/Tonel/error-handling-layer-react" Then, launch the React application on your machine with:

cd "error-handling-layer-react"npm installnpm run start Great! You now know everything you need to know about handling errors in React!

ConclusionIn this tutorial, you learned what an error handling layer is, its essential benefits in the frontend world, and how to integrate it into a React application. An error handling layer is nothing more than an error boundary component that catches and handles errors gracefully. This allows you to consolidate error handling logic into a single layer of the React application architecture, making the debugging easier. As shown in a complete example, implementing an error handling layer in React is simple and takes only a few minutes.

The post Building an Error Handling Layer in React appeared first on Semaphore.

View Details

In the tech industry, diversity and inclusivity have been long-standing challenges. Head of Engineering Florence Chabanois understands the importance of creating a work environment that celebrates differences and the role of management in addressing inclusion issues. With over a decade of experience in tech, Florence shares her insights on team management, onboarding new members, accommodating microcultures, and more.

Edited transcriptionFlorence Chabanois is the head of engineering for Transversal Teams at Scaleway. To become Europe’s top cloud provider, Scaleway’s ecosystem includes a long list of services made up of data centers, serverless architecture, managed services, bare metal solutions, and more. Florence plays a prominent role in fulfilling this objective; currently, she supervises over 40 people, including developers and engineering and project managers.

Agile transversal teams such as Florence’s bring together individuals from different departments to collaborate on specific projects or tasks to increase collaboration, creativity, and efficiency. Consequently, effective management and coordination are essential for these teams to function smoothly.

Onboarding and alignmentGrowing companies recruit employees to form new teams or expand old ones. The challenge is getting new team members to accustom themselves to the organization’s culture while also giving them the confidence to raise their own voices. On the other hand, old employees need to readjust to the reorganization of their teams.

According to Florence, newcomers will adapt more easily, since they don’t have any prior working habits in the company to leave behind, something old employees will need to do. However, she also emphasized the importance of letting newcomers know about the company’s history, learning what worked when it was smaller, and what ideas have already been put into practice. This knowledge will help the team have a different point of view and a deeper understanding of how the company operates and what has and has not been tested.

From a broad perspective, Florence understands that alignment between all parties of a company is what will make it succeed; from team members to the CPO, the company needs to be aligned about what they want to achieve and how they want to achieve it. Since communication is the only way to ensure everyone is on the same page, it should be done repeatedly to ensure the message goes through. “I heard that we have to repeat something at least three times so that maybe it’ll be heard once,” Florence says.

When screening for new hires, Florence looks for soft skills, such as interpersonal communication abilities. While in the past she focused more on technical skills, she believes these can be acquired eventually. while soft skills are harder to learn.

In like manner, Florence calls for finding potential good teammates and avoiding toxic people from the start. Applying this principle is even more important in companies like hers where teams are made of people from all over the world and with different native languages. “The ability to learn things is important, and to change, to adapt, rather than the fact that you know the language by heart,” Florence affirms.

Therefore, screening questions should inquire candidates about what they would do to make sure everyone feels safe and how they could contribute to creating a happy working environment. The ideal work environment should respect the authenticity and identity of each team member. “It’s about how we include and how we develop tools for everyone, instead of always the same category of people,” Florence concludes.

The role of managers: Helping teams succeed through direction and communicationFlorence believes that the role of a manager can be summed up as helping others. In other words, “you have a less direct impact and more human things to deal with,” she says. While helping the team can sometimes take the form of menial tasks such as preparing coffee, managers should excel at helping their teams by providing direction and making decisions.

What’s more, managers should look after team members and ensure they feel comfortable and encouraged to speak up and contribute. “It can be about being loud, but it’s about being heard,” Florence says. In this regard, she recommends enabling different communication channels, such as written documentation or one-on-ones, so each one can choose to communicate in the way they find most suitable.

For those engineers who are moving towards managerial roles such as project manager or head of engineering and want to learn how to approach leadership, Florence recommends reading “Radical Candor” by Kim Scott. The book emphasizes building solid relationships with team members and providing clear, compassionate feedback while offering practical advice and actionable strategies for effective management. When Florence started as a manager, Radical Candor helped her figure out how being nice or listening to people didn’t conflict with her role as a figure of authority.

On the other hand, Florence’s biggest advice for those soon to become managers is “to belong to a network and to have peers to exchange with.” In this way, you can learn from those who have experience in the job and can let you know if it is something that you want to do.

The impact of organizational cultureAn organization’s culture has a significant impact on the success of its software engineering teams. When successfully written and communicated, it will lead to better collaboration, innovation, accountability, and continuous learning.

However, the culture of the whole organization can be at odds with the microculture created and sustained by a department, a whole team, or part of it. Both should coexist harmoniously. Florence believes that if individuals were more proud of their unique perspectives, they could create a more inclusive product and attract a more diverse clientele. However, she notes that often people feel ashamed of their differences, which hinders their ability to communicate and collaborate effectively.

Florence affirms that accommodating different opinions isn’t a matter of holding group discussions, but one-on-ones since this is “where you can have an opportunity to know people better and what’s important for them.” Due to their importance, one-on-ones should be maintained even while working remotely. In this regard, nowadays there are emerging tools for getting remote employees to know each other and interact more casually. Still, Florence is wary of these tools, arguing that they can’t replace what meeting face-to-face creates. For this reason, Scaleway, despite having a policy for working fully remotely, also has a policy in which employees have to go to the office at least once a month. At the same time, Florence also encourages outdoor activities, such as walking. In any case, she believes it is necessary “to really think outside the box instead of taking things that we know at work.”

Withal, despite it being crucial to respect different opinions, these can’t oppose the culture of the organization. In this regard, someone whose views differ radically from the organizations’, such as racist views, won’t have a place in it.

Florence’s message is especially relevant in the tech industry, which has historically struggled with diversity and inclusivity. By acknowledging and celebrating differences, tech companies can create a more innovative and creative work environment, which will ultimately lead to better products and services. Florence’s feedback loop concept emphasizes the importance of continuously seeking feedback from diverse perspectives to find better solutions and a more successful business overall.

To this end, companies need to create a work environment in which their employees can speak their minds about the product they are working on. Florence understands feedback is a gift since it wants to improve things and change something that the person disagrees with. However, it should be noted that giving feedback takes courage, even more so if you’re a minority. Florence asks organizational cultures to be open to feedback and encourage it by being thankful and open about appreciating it.

The bottom lineAs part of her feminist works and inclusion in the software industry, Florence is part of DuchessFrance, Tech.Rocks, and La Place Des Grenouilles communities. You can follow Florence on Twitter and Mastodon.

The post Florence Chabanois on Creating a Diverse and Inclusive Work Environment appeared first on Semaphore.

View Details

The Microservices Architecture is an evolved pattern that has fundamentally changed the way server-side code is developed and managed. This architectural pattern involves the design and development of the application as a collection of loosely-coupled services that interact over well-defined, lightweight APIs to meet business requirements. It aims to help software development firms to accelerate the development process by facilitating continuous delivery and development.

If we talk about its elemental level, a particular microservice acts as an application in and of itself that forms a larger application with other microservices; this enables:

  • Easier and faster development
  • Maintainability
  • Scalability

Essentially, this allows you to manage and maintain an application more effectively. There is, however, specific complexity inherent to this pattern, which can be mitigated by using certain best practices.

We all know that microservice design has a direct impact on the network resilience of modern architectures. When businesses decide to build using microservices, it’s important to develop them efficiently and effectively so that they can operate over the network, without causing excess latency, bandwidth consumption, and packet loss.

In this article, we’re going to discuss the basic microservices best practices that you should consider if you want to achieve an efficient microservices ecosystem devoid of extreme architectural complexities. So, without any further ado, let’s get started.

Adopt the Single Responsibility PrincipleThe Single Responsibility Principle is the concept that any single object in OOP should be made for one specific function. Basically, it is part of the programming principles put forth by Robert Martin. Just like with code, a class should have only one reason to change, making the software more maintainable, scalable, and easier to understand. To adopt SRP in software development, you should ensure that each class or module has a well-defined responsibility and that it is not trying to do too many things. You should also keep your modules decoupled and use clear and concise interfaces to communicate between them. To summarize this, we have an interesting quote:

Gather together those things that change for the same reason, and separate those things that change for different reasons.” – O’reilly

We can say that this is one of the finest and most essential principles to building a good architectural design as it signifies that a microservice, module, class, subsystem, or function should not have multiple reasons to change.

Let’s understand this Principle with an example:

An eCommerce portal might have a microservices architecture like this:

Here, all the services (e.g. Product Listing Service, Order Service, Customer Service, Payment Service, Cart Service, Wishlist Service, etc.) have single responsibilities. This means that it is important to make sure that you’re not integrating one service with another when it is not absolutely necessary, as it makes the architecture more complicated to maintain and test.

Build teams with clear responsibilitiesTo develop the microservices architecture, we need to build teams that have clear responsibilities. This can be done in multiple ways, like role-based teams, cross-functional teams, etc. In this architecture, each microservice works as an independent application. So, each team should be versatile enough to handle their operations.

Let’s understand this with an example:

An organization can have role-based teams like UI/UX developers, front-end developers, back-end developers, database admins, QAs, middleware developers, etc. that work in isolation but interact on a daily basis via meetings–either in person or using various communication tools like JIRA, Slack, and so on.

When we think about maintenance, sometimes small bugs or sometimes even large bugs can also occur in the system. So, SCRUM can be a possible solution. It helps each team member to shorten the time of unawareness. But as the teams are organized based on the roles, integration of one update within one sprint can become a complicated task. For example, if UI/UX developers don’t get any information from the server guys about changes in an API, the new API won’t be useful at all.

So what’s the solution?

Build cross-functional teams with clear responsibilities, to help orchestrate work between teams

One cross-functional team that is responsible for the whole microservices functionality can be a major benefit to your project. This team should consist of members from all of the role-based teams and is responsible for orchestrating the various parts of the application, i.e. UI, development, database, and even QA. If there are two versions of the application, i.e. web and mobile, then developers from both the teams should be present in this team. The key benefit of this kind of team is that it becomes easy to solve bugs, develop new features and deploy them in the production environment.

Use the right tools and frameworksBy this point, you have probably designed your microservices to deploy them independently, now you must realize the optimal value of those microservices. And to do that, you need to automate build and deployment management using a good set of DevOps tools.

Using the right tools, frameworks, and libraries will go a long way towards implementing a microservice architecture. If you’re planning on doing this in Java, consider the Spring Boot Project. Choosing the right tools and frameworks takes a lot of time and effort so here’s a list of “go-to,” proven tools and technologies for the job:

  1. Jenkins and Bamboo for deployment automation
  2. Docker for containerization
  3. Postman for API testing
  4. Kubernetes for container orchestration and deployment
  5. Logstash for monitoring
  6. DevSecOps to manage the entire process of the software development lifecycle
  7. GitHub for source code management and version control
  8. Amazon’s simple queue service for messaging
  9. SonarQube to check the code quality and security
  10. Ansible for managing your configuration
  11. Jira for issue tracking and project management

Keep asynchronous communication between microservicesTwo types of communications occur between microservices: Synchronous and Asynchronous. Let’s understand this with an example:

For an ecommerce platform, Synchronous communication means that the user will be required to “stay on the line” and advance through a series of steps (select items, add delivery address, payment details, order verification) ultimately resulting in the customer notification “Thank you for your order! We’re delivering it next week”.

There are several async communications that also occur once the customer notification is processed that are part of the “fulfillment” stage of the order like: warehouse notification, inventory update, etc.

In the case of synchronous communication, one service becomes dependent on another service. Sometimes, it becomes a time consuming process to complete the entire task using synchronous communication between multiple microservices.

On the other hand, async communications are not dependent on one another. So, every service can take its time to complete its task. Therefore, one should try to maximize async communication between microservices wherever possible. It reduces dependency and increases the overall efficiency of an application.

You can see an example of this below:

Adopt the DevSecOps model and secure microservicesSecurity is very important in this architecture. As microservices architecture has evolved in developing cloud native applications, DevSecOps practices are increasingly used to ensure continuous integration and continuous delivery with increased security measures. An application build using microservices can be divided into the following code types:

  1. Application code (core logic)
  2. Application service code (network connections, session establishment, etc.)
  3. Infrastructure (data storage resources, networking, platforms, etc.)
  4. Monitoring (continuous observability of the application)

DevSecOps consists of three concepts: development, security, and operations, and has proven to be a facilitating paradigm for code types with primitives such as continuous integration, continuous delivery, and continuous deployment pipelines. These pipelines are workflows for using developers’ source code for developing, testing, deploying, and many such operations that are supported by automated tools with feedback mechanisms. Also, it makes development teams deliver better, more-secure code faster. DevSecOps practices in microservices architecture provide numerous benefits such as:

  • High security assurance
  • Reduction in code vulnerability
  • Enhanced product quality
  • Increased productivity
  • Increased speed of the operations
  • Deliver better and higher quality software faster

Use a separate data store for each microserviceOne important practice is to make sure to have a separate database to store data whenever possible, rather than having the same database for multiple microservices such as in a monolithic architecture. However, a more in-depth analysis might indicate that one microservice only works with a subset of database tables whereas, on the other hand, another microservice only works with an entirely new subset of tables. And if both subsets of data are orthogonal, this would be a case for separating the database into separate services. Therefore, make sure to have a separate data store for your microservices to reduce latency and improve security. This has been mentioned many times already, but it is important to emphasize that microservices should depend on each other as little as possible.

One of the main attributes of microservice architecture is that each service’s data is private, as is the case, for instance, with the Database per Service pattern.

We can also use a shared database server that can be used by multiple services with a logical separation of their data.

Deploy each microservice separatelyIf you’re deploying each microservice separately, you’re surely going to save a lot of time coordinating with multiple teams while maintaining or upgrading efforts. Also, if one or more microservices have the same resources, we recommend you use a dedicated infrastructure to isolate each microservice from faults and avoid a full-blown outage.

Some of the most common and popular patterns for deploying microservices are:

  1. Multiple service instances per host
  2. Service instance per container
  3. Single service instance per host
  4. Service instance per VM

Orchestrating microservicesOrchestration of your microservices is one of the most influential factors to gain success in both the process and tooling. You can use Docker to run containers on a VM but it won’t provide the same level of resilience that a container orchestration platform offers. Such a decision may very well negatively affect your uptime while trying to adopt a microservices architecture.

Here are some of the orchestrating platforms that are proven:

  • K8s (Kubernetes)
  • AKS (Azure Kubernetes Services)
  • ECS (Amazon Elastic Container Services)
  • Azure Container Apps

These platforms can be helpful in managing container provisioning & deployment, load balancing, scaling, network communication concerns, etc.

Use an effective monitoring systemThe microservices architecture helps you to carry out enormous scaling of thousands of modular services and offers the potential for improved speed and organized methods of monitoring. It is important, however, to make sure to review all your microservices and regularly check if they are functioning as desired and are efficiently using the available resources. Depending on these observations, you can take appropriate actions if expectations are not being met.

Let’s analyze an example situation and imagine that you have applied a microservices architecture pattern which does not have the ability to handle requests, but they are still running. For example, if it runs out of database connections, the monitoring system should be capable of generating an alert whenever an instance fails and requests should be routed to working service instances.

Monitoring microservices, and keeping these stats explained precisely will help you to improve decision-making and keep your microservices available when needed.

Let’s have a look at a few examples of microservices monitoring tools.

AWS CloudWatch: a monitoring, observability, and management service that collects and visualizes real-time logs and provides actionable insights for AWS, hybrid, and on-premises applications and infrastructure resources.

Jaeger: software designed to monitor and troubleshoot complex problems in a microservice environment.

Datagod: an observability, security, and analytics platform for cloud-scale applications that provides a comprehensive solution for databases, services, and tools using an SaaS-based data analytics platform.

Graphite: as the name suggests, it is open-source software that monitors and graphs numeric time-series data and provides in-depth insights into the underlying system.

Prometheus: a free and open-source software tool that offers monitoring and modification solutions.

ConclusionSo that’s it for this post. I hope that you found this reading useful and that you’ll follow these microservices’ best practices to end up with an independent, loosely-coupled system, in order to reap the benefits of this architecture.

The post Microservices Best Practices appeared first on Semaphore.

View Details

Any frontend application will need to manage its own state in order to display data for the user. React offers the Context API out of the box, getting rid of the need to pass specific props manually at every level in the component tree. It enables us to make certain data available globally without needing to introduce third-party libraries such as Redux.

In addition, by using the Context API, we get rid of a nasty problem: prop drilling. This term refers to passing state from a parent component down the component tree until it reaches a deeply nested component. Not only does this practice clutter our codebase, but it also becomes difficult to maintain.

In this article, we are first going to learn how to use Context and how to access the data that it exposes down the component tree. Afterwards, we’ll write custom hooks to avoid prop drilling by leveraging the Context API and efficiently managing state in a scalable way.

How to use ContextThe first step in using this API is to create a context, as shown below:

const CatFactsContext = createContext<CatFactsContextProps>({ catFacts: [],}); CatFactsContext is the object that contains our context. It offers a Provider component that we will use for wrapping our component tree. In this way, everything that the context contains will be available to all its children. In our case, cat facts will be available to the entire component tree.Now, let’s make use of the built-in hook useContext:

const CatFactDisplay = () => { const { catFacts } = useContext(CatFactsContext); return ( <ul> {catFacts?.map((fact, index) => ( <li key={index}>{fact.text}</li> ))} </ul> );}; In the snippet above, we consume the context from the provider. We are getting the facts array, which we then display to the user.

Here is the complete code for this example:

import React, { createContext, useContext, useEffect, useState } from 'react';interface CatFact { text: string;}interface CatFactsContextProps { catFacts: CatFact[];}const CatFactsContext = createContext<CatFactsContextProps>({ catFacts: [],});const CatFactDisplay = () => { const context = useContext(CatFactsContext); if (!context) { return null; } const { catFacts } = context; return ( <ul> {catFacts?.map((fact, index) => ( <li key={index}>{fact.text}</li> ))} </ul> );};const App = () => { const [catFacts, setCatFacts] = useState<CatFact[]>([]); useEffect(() => { const fetchData = async () => { const response = await fetch('https://cat-fact.herokuapp.com/facts'); const data = await response.json(); setCatFacts(data); }; fetchData(); }, []); return ( <CatFactsContext.Provider value={{ catFacts }}> <CatFactDisplay /> </CatFactsContext.Provider> );};export default App; Generally, we’d use Context in order to pass down some info such as the currently selected language, the user name, and the theme variant (dark/light). But what if we could also make use of useContext to reuse code?

Code reusability with ContextLet’s say that we need to write a confirmation dialog to use in our app. It will probably have a title, some content, and two buttons (accept and cancel, probably). This dialog will show up everywhere in our app where we need the user to confirm a certain action. An obvious solution for accomplishing this is to add this component to every component where it is needed, but this could clutter up our code base. Instead, let’s take a look at a solution that uses Context and custom hooks.

Let’s create a file named ConfirmationDialogContext where we will start working on our context provider and our custom hook. We are going to start by defining the relevant types and then we will create the context:

type ConfirmationDialogState = { isOpen: boolean; confirmAction: () => void;};type ConfirmationDialogContextValue = { setDialogState: (state: ConfirmationDialogState) => void;} & ConfirmationDialogState;const ConfirmationDialogContext = React.createContext< ConfirmationDialogContextValue | undefined>(undefined); As you can see, ConfirmationDialogState contains the fields that are relevant for the dialog itself (such as being open or not, as well as what to do when the user confirms the action), whereas ConfirmationDialogContextValue will allow us to put these props inside of our provider.

The provider itself will be a component that will accept children props, which will be the component tree for our app since this provider needs to be at the top level. Inside this component, we will keep the state of our dialog (isOpen and confirmAction), and we will return the provider for the ConfirmationDialogContext object that we have created above:

const ConfirmationDialogProvider = ({ children }: { children: ReactNode }) => { const [dialogState, setDialogState] = useState<ConfirmationDialogState>({ isOpen: false, confirmAction: () => null, }); const confirmAction = () => { dialogState.confirmAction && dialogState.confirmAction(); setDialogState({ isOpen: false, confirmAction: () => null }); }; return ( <ConfirmationDialogContext.Provider value={{ ...dialogState, setDialogState, confirmAction }} > {children} </ConfirmationDialogContext.Provider> );}; The custom hook that we will use in our components for making use of the confirmation dialog will look like this:

const useConfirmationDialog = () => { const context = React.useContext(ConfirmationDialogContext); if (!context) { throw new Error( 'useConfirmationDialog must be used within a ConfirmationDialogProvider' ); } return context;}; Just like in our cats example, we are returning the context so that we can access it as needed using the useContext hook. We need the nullability check to ensure that we do not work with an undefined context.

At the end of our file, we can export the provider and the custom hook:

export { ConfirmationDialogProvider, useConfirmationDialog }; For the sake of simplicity, let’s go to our App.tsx file (or wherever the App component is defined in your project) and wrap our children with the provider. The children, in this case, will just be a small component we will write above our App. This component will have a button that will open the confirmation dialog.

import React from 'react';import { ConfirmationDialogProvider, useConfirmationDialog,} from './ConfirmationDialogContext';import ConfirmationDialog from './ConfirmationDialog';const ConfirmationExample = () => { const { isOpen, setDialogState, confirmAction } = useConfirmationDialog(); const handleButtonClick = () => { setDialogState({ isOpen: true, confirmAction: () => console.log('Confirmed'), }); }; return ( <div> <button onClick={handleButtonClick}>Open Confirmation Dialog</button> {isOpen && ( <ConfirmationDialog confirmAction={confirmAction} cancelAction={() => setDialogState({ isOpen: false, confirmAction: () => null }) } /> )} </div> );};const App = () => ( <ConfirmationDialogProvider> <ConfirmationExample /> </ConfirmationDialogProvider>);export default App; The ConfirmationDialog component could look something like this:

import React from 'react';type ConfirmationProps = { confirmAction: () => void; cancelAction: () => void;};const ConfirmationDialog = ({ confirmAction, cancelAction,}: ConfirmationProps) => ( <div> <p>Are you sure you want to proceed?</p> <button onClick={confirmAction}>Yes</button> <button onClick={cancelAction}>No</button> </div>);export default ConfirmationDialog; And this is it. Now we can invoke the useConfirmationDialog in any component where we need the confirmation of the user. This leads to more readable code and it allows us to avoid adding the ConfirmationDialog component throughout the entire code base.

Here is the complete confirmation dialog example if you would like to try it out.

ConclusionIn this article, we understood what the Context API is and how it is generally used in React. We looked at a basic example with the createContext and useContext hooks in order to understand how to provide values using the Context provider, and then we went over a more advanced use case of the context provider: creating a component that enabled us to reuse code by the means of a custom hook.

Now you should be able to use Context to avoid prop drilling in your project, as well as finding more advanced use cases where this API could simplify your code.

The post Mastering the Context API in React for State Management and Advanced Use Cases appeared first on Semaphore.

View Details

Data pipelines are a series of processes that move data from one stage of processing to the next. These processes can include collecting, cleaning, transforming, and storing or analyzing data.

The goal of a data pipeline is to efficiently and effectively move data through these stages to make it available for further analysis or use.

As developers, being aware of how to use and implement data pipelines can be invaluable in today’s ecosystem; for example:

  • Data pipelines are an effective way to manage and process large amounts of data.
  • Data pipelines are essential for building data-driven applications. Many applications need to implement things like personalization, analytics, etc.
  • Data pipelines are a key component in distributed systems and are becoming increasingly common as the industry adopts this architecture more frequently.

When implementing data pipelines, there are many options available to developers. This article doesn’t aim to be a full guide of all the options, rather it focuses on understanding how Elixir and its ecosystem can be used to implement data pipelines directly without relying on complex systems like Kafka.

Why Elixir?Elixir is a good language for implementing data pipelines because it is designed for concurrent and parallel processing. This means that it can handle multiple tasks simultaneously, making it well-suited for handling large amounts of data.

Additionally, Elixir has built-in support for functional programming, which allows for clear and expressive code that is easy to reason about and test. The language also has a robust ecosystem of libraries and frameworks that can be leveraged for data processing and pipeline management tasks.

Furthermore, Elixir runs on the Erlang VM, which is known for its reliability and fault-tolerance, making it ideal for data pipelines that require high availability and performance. Additionally, Elixir’s message-passing concurrency model allows for easy distribution and scaling of data pipelines, enabling them to handle large amounts of data and traffic.

In this tutorial, we will show you how to use the Flow library in Elixir to build a data pipeline. We will cover the following topics:

  • Setting up the environment and installing the necessary dependencies
  • Creating a new Elixir project and configuring it for data processing
  • Defining the stages of the pipeline using the Flow library
  • Building and running the pipeline
  • Testing and debugging the pipeline

Setting up a data pipelineBefore we begin, it is assumed that you have a basic understanding of Elixir and functional programming concepts. If you are new to Elixir, it is recommended that you familiarize yourself with the basics before proceeding.

The complete code for this tutorial can be found in the GitHub repository.

Setting up the environmentTo use the Flow library, you will need to have Elixir and Erlang installed on your machine. You can download the latest versions of these languages from the official Elixir website.

Once you have Elixir and Erlang installed, you can create a new Elixir project by running the following command in your terminal:

$ mix new catalog_pipeline This will create a new directory called “catalog_pipeline” that contains the basic structure of an Elixir project.

Next, you will need to add the Flow library; and a few extra libraries to handle web requests as dependencies to your project. You can do this by ensuring the deps function of your mix.exs file looks like:

defp deps do [ {:flow, "~> 1.0"}, {:httpoison, "~> 1.7"}, {:poison, "~> 4.0"}, ]end Then, run the following command in your terminal to install the dependency:

$ mix deps.get Defining the stages of the pipelineNow that we have the Flow library installed, we can start defining the stages of our pipeline. The Flow library provides a set of pre-built stages for common data processing tasks such as filtering, mapping, and reducing. These stages can be used out of the box, or developers can create their own custom stages for more specific tasks.

For the purposes of this tutorial, we will use the Flow library to build a data pipeline that extracts product data from dummyjson.com, transforms it into a tuple with description, title, and price, validates certain fields and finally prepare the data to be loaded into a database or a csv. The pipeline will consist of several stages, as detailed in the following sections

Data ExtractionThe first step in building a data pipeline using Elixir and the Flow library is to extract the data that we want to process. This can involve a variety of tasks, such as fetching data from a database, web service, or file system.

One way to extract data using Flow is to use the Flow.from_enumerable/2 function, which allows you to create a flow from any Enumerable data structure, such as a list or map. For example, you could extract data from a database table by querying it and then passing the results to Flow.from_enumerable/2.

Let’s define the first stage and load product data from dummyjson.com by overwriting your lib/catalog_pipeline.ex file to look like:

defmodule CatalogPipeline do use Flow def get_products do product = extract_product_data('https://dummyjson.com/products') end defp extract_product_data(url) do extracted_data = HTTPoison.get!(url) |> Map.get(:body) |> Poison.decode!() |> Map.get("products") |> Flow.from_enumerable() endend The first step is to define a module that will contain the pipeline; we will call it CatalogPipeline. Next, we will define a function called get_products that will return the product data. This function will call the extract_product_data function, which will use the Flow library to extract data from the dummyjson.com API.

The extract_product_data function will use the HTTPoison library to make a GET request to the dummyjson.com API and then use the Poison library to decode the JSON response. The decoded data will be passed to the Flow.from_enumerable/2 function, which will create a flow from the data.

You can already see the products within the Flow enumerable object by running $ mix compile and then $ iex -S mix. Within the interactive prompt, issue iex(1)> CatalogPipeline.get_products. Remember to Ctrl-C and (a)bort to get back to the shell.

Data TransformationThe next step in building a data pipeline is to transform the data that we’ve extracted. This can involve a variety of tasks, such as cleaning, filtering, or aggregating the data.

One way to transform data using Flow is to use the Flow.map/2 function, which allows you to apply a function to each item in a flow and return a new flow with the results. For example, you could clean up the data by removing any fields that are not needed.

To continue our example, we will define a new function called transform_product_data that will transform the data into a tuple with a description, title, and price:

defmodule CatalogPipeline do use Flow def get_products do product = extract_product_data('https://dummyjson.com/products') |> transform_product_data() end defp extract_product_data(url) do extracted_data = HTTPoison.get!(url) |> Map.get(:body) |> Poison.decode!() |> Map.get("products") |> Flow.from_enumerable() end defp transform_product_data(extracted_data) do transformed_data = extracted_data |> Flow.map(fn product -> {product["description"], product["title"], product["price"]} end) endend This stage of the pipeline uses the following steps:

  • Flow.map to extract the title, description and price of each product and return them as a tuple.

Data Quality AssuranceFinally, it is important to check the quality of the data before it is loaded into the target system. In Elixir, we can use Flow.filter to filter out any invalid or incomplete data, or even remove any rows that do not have a valid price and description.

defmodule CatalogPipeline do use Flow def get_products do product = extract_product_data('https://dummyjson.com/products') |> transform_product_data() |> validate_product_data() end defp extract_product_data(url) do extracted_data = HTTPoison.get!(url) |> Map.get(:body) |> Poison.decode!() |> Map.get("products") |> Flow.from_enumerable() end defp transform_product_data(extracted_data) do transformed_data = extracted_data |> Flow.map(fn product -> {product["description"], product["title"], product["price"]} end) end defp validate_product_data(transformed_data) do validated_data = transformed_data |> Flow.filter(fn {description, title, price} -> description != "" and price != "" end) endend Adding this stage for filtering the transformed data where the price is not available ensures that the final data is clean and usable, reducing the risk of errors downstream.

Data LoadingThe final step in our pipeline is to load the transformed data into a target system. This can involve a variety of tasks, such as inserting the data into a database, writing it to a file, or publishing it to a message queue.

For this last part of the example, we will parse the list of products into a list that we can either insert into a database or write to a file.

defmodule CatalogPipeline do use Flow def get_products do product = extract_product_data('https://dummyjson.com/products') |> transform_product_data() |> validate_product_data() |> load_product_data() |> IO.inspect() end defp extract_product_data(url) do extracted_data = HTTPoison.get!(url) |> Map.get(:body) |> Poison.decode!() |> Map.get("products") |> Flow.from_enumerable() end defp transform_product_data(extracted_data) do transformed_data = extracted_data |> Flow.map(fn product -> {product["description"], product["title"], product["price"]} end) end defp validate_product_data(transformed_data) do validated_data = transformed_data |> Flow.filter(fn {description, title, price} -> description != "" and price != "" end) end defp load_product_data(validated_data) do product_list = validated_data |> Flow.map(fn {description, title, price} -> %{title: title, price: price, description: description} end) |> Enum.to_list() endend With this last stage, we are loading the data into a list, which depending on the application, can be used to insert the data into a database or write it to a file.

If everything goes well, you should see a list of products printed on the console.

[ %{ description: "An apple mobile which is nothing like apple", price: 549, title: "iPhone 9" }, %{ description: "SIM-Free, Model A19211 6.5-inch Super Retina HD display with OLED technology A12 Bionic chip with ...", price: 899, title: "iPhone X" }, %{ description: "Samsung's new variant which goes beyond Galaxy to the Universe", price: 1249, title: "Samsung Universe 9" }, %{ description: "OPPO F19 is officially announced on April 2021.", price: 280, title: "OPPOF19" }, %{ description: "Huawei’s re-badged P30 Pro New Edition was officially unveiled yesterday in Germany and now the device has made its way to the UK.", price: 499, title: "Huawei P30" }, ...] ConclusionThis article taught you how to build a data pipeline using Elixir and Flow. You have seen how to extract data from an API, transform it, validate it, and load it into a target system. We have also seen how to use Flow to build a pipeline that can be easily extended and reused.

However, we have only scratched the surface of what is possible to do with Elixir and Flow; there are many more features and capabilities than can be covered in this article. If you want to learn more about Elixir and Flow, you can check out the following resources:

  • Introduction to Flow
  • Introduction to the Elixir Flow library
  • Using Flow in Elixir to Easily parallelize data processing

Finally, as you continue on your journey to use Elixir and Flow to build data pipelines, it is important to keep in mind that there are some key differences between data pipelines and ETL (Extract, Transform, Load) pipelines:

  • Data Pipelines is an umbrella term, and ETL pipelines are just a subset of data pipelines.
  • An ETL pipeline will always involve transforming the data, while a data pipeline may not.
  • Data pipelines run continuously, while ETL pipelines run only once or in batches.

The post Creating Data Pipelines With Elixir appeared first on Semaphore.

View Details

React Native has seen a great market adoption since its inception and has come a long way in terms of creating super fast and performant apps. However, keeping apps performant is a marathon and it gets harder as we scale our apps. To keep apps performant, constant monitoring and optimization is necessary.

There are many factors that affect the performance of an app. Some may impact app startup time while others may impact the app’s runtime performance. In this post, we will go through some of the tools and techniques that can help you improve and maintain the performance of your React Native apps.

How important is performance?Performance in today’s world isn’t just the cherry on top of the cake. It is a necessity. Recent studies and statistics show that 70% of users will abandon an app in favor of a competitor, if it loads slowly. According to a study by Google, even the slightest delay in app loading can lead to a significant drop in user engagement. Or, on the flip side, if an app loads fast, it can lead to a significant increase in conversions and better user retention.

With the rise of mobile apps, the competition is getting tougher and tougher, studies show that nearly one in every two apps are uninstalled within the first month of installation. Poor performance can be a deal breaker for your app.

Tools & techniques to improve performanceHere’s some of the tools and techniques that can help you improve the performance of your React Native apps.

Enable HermesHermes is a new JavaScript engine optimized for running React Native apps; its goal is to improve app performance. It converts JS code to bytecode that enables faster startup time. Also, it does better memory management through improved garbage collection to reduce memory usage and improve runtime performance.

Impact: Better Startup time, lesser memory usage, better runtime performance.

New ArchitectureReact Native went through a massive architectural overhaul in the past couple of years, resulting in a whole new architecture based on Fabric and TurboModules. I discussed this new architecture in another post. As a recap, this new architecture mitigates one of the most critical performance bottlenecks in React Native: “the bridge”, using TurboModules. Fabric, a new rendering mechanism, brings features like view flattening, render pipeline along with React Concurrent Features to make UIs smoother and more responsive than ever before.

Impact: Better UI rendering for a smoother experience

Optimizing JS Bundle using Re.packFor large scale apps, JS bundle size can be a big concern. As the app grows, the bundle size grows as well. This can lead to slower app startup time and slower runtime performance. As most of the logic for RN apps is written in JS, optimizing the JS bundle can have a huge impact on the performance of the app.

Using Re.pack can help you create multiple chunks of JS bundle and load them on demand. This can have a direct impact on startup time, as less of the JS bundle is loaded initially. Also, this can improve runtime performance, as less of the JS bundle is loaded in memory.

Impact: Smaller app size, faster startup, better runtime performance

Graphics & ImagesTo be on top, aesthetics and visuals in apps are essential. On the performance side, it can be challenging to handle the impact that high resolution images or complex graphics can have on the app. To keep the app performant, it is important to optimize images and graphics. On the simpler side, all images in the app can be passed through an optimizer to reduce their size while keeping quality intact. While the better solution would be to break down images into more manageable chunks and render them accordingly. Here are small tricks to manage images and graphics in your app.

  • Gradients: for apps rendering gradients, images can be avoided in favor of rendering gradients directly in the app using react-native-linear-gradient.
  • SVGs: Vector graphics are more scalable and optimized compared to raster graphics. For images that can be vectorized, using SVGs can bear fruit. Render SVGs directly in the app using react-native-svg; there is also a great tool to embed gradients in SVG paths.
  • Skia: For apps with high graphics requirements, Skia can be a good solution. Skia is a cross-platform 2D graphics library built by Shopify. It can be a drop-in replacement for libraries like react-native-svg and react-native-linear-gradient. It offers a wide range of performant graphics rendering solutions.
  • React Native Fast Image: For apps with a large number of images, using Fast Image can be a good solution. It is a wrapper around SDWebImage (iOS) and Glide (Android). It offers aggressive caching and prefetching of images to improve performance. It also enables features like border radius, gif support, authorization headers, etc. to make image usage more flexible.
  • Use optimized images in better formats; for example; using WebP images can reduce the size of images by 30% while keeping the quality intact. Also, using SVGs can reduce the size of images by 90% while keeping the quality intact.

Impact: Better performance for images and graphics, smaller app size, lesser network cost

Optimizing large listsFor apps with large lists, it can be challenging to keep the frame rate smooth, especially when the list is scrolled. Also, when the list is scrolled, the app is more likely to run out of memory. To keep the app performant, it is important to optimize memory usage and frame rate. Here are some of the techniques that can help you optimize large lists.

  • Flashlist – A drop-in replacement of Flatlist, with 5x improvement on UI thread and 10x on JS thread. It does better memory management by avoiding recreation of items as users scroll through the list. It also offers features like sticky headers, pull to refresh, etc.
  • RecyclerView – A JS-only list view, inspired by both RecyclerView on Android and UICollectionView on iOS. It offers a range of features to create aesthetically pleasing lists with smooth scrolling and memory optimization. It is battle tested at Flipkart and has been used in production.
  • Optimized Flatlist – React Native’s default list component offers a range of options to ensure both memory and frame rate optimization. If you are using Flatlist, make sure to use getItemLayout prop to optimize memory usage. Also, use initialNumToRender and maxToRenderPerBatch props to optimize frame rate.

Impact: Better performance for large lists, lesser memory usage, better frame rate

AnimationsLike graphics, animations are also a big part of great user experience. At the same time, Janky animations can be a big turn off for users. Performance of animations is one of the most critical vectors in app performance. Implementing animations in a performant way can involve some complex math and algorithms. But, even with that, it boils down to how well the implementation of animations is done on the platform. Here are tools to handle complex animations in a performant way.

  • React Native Reanimated – It aims to provide ways of offloading animation and event handling logic off of the JS thread to the UI thread. This is achieved by defining Reanimated worklets – tiny chunks of JavaScript code that can be moved to a separate JavaScript VM and executed synchronously on the UI thread. This makes it possible to respond to touch events immediately and update the UI within the same frame when the event happens, without worrying about the load that is put on the main JavaScript thread. Thus animations built with Reanimated are smoother and more responsive even on low-end devices.
  • Lottie – Lottie is a library for Android, iOS, Web, and Windows that parses Adobe After Effects animations exported as json with Bodymovin, and renders them natively on mobile and on the web. It also takes away the complexity of creating animations from scratch and provides a wide range of animations that can be used in the app. It also enables designers to plug their animations into the app without any coding.

Impact: Smoother animations, better user experience

AdaptabilityPerformance and adaptability go hand in hand. For a great user experience and to keep users engaged, it is important for apps to adapt to the device’s capabilities at any given time. For example, if the device is low on memory, the app should be able to adapt to it and not crash. Similarly, if the device is low on battery, the app should be able to adapt to it and not drain the battery.

  • React Native Device Info – A library to get device information like memory, battery, etc. It can be used to get device information and adapt the app accordingly. For example, if the device is low on memory or battery, the app can adapt by not loading complex graphics and falling back to simpler ones. Or, in some cases, render lesser pages ahead in the list, or avoid styles that consume more memory like shadows, etc.
  • React Native Netinfo – Adapting to network quality is also important for data heavy apps. Using this library, the app can get information about the network connection and its quality. Leveraging this information, it can defer unnecessary network calls to consume lesser bandwidth or avoid features like prefetching that consume more bandwidth.

Impact: Better user experience, lesser crashes, lesser battery drain

GesturesGestures are a big part of the user experience. It is important to make sure that gestures are smooth and responsive. Having lag in gestures makes the app feel unresponsive and can result in drop off. Thanks to React Native Gesture Handler, performant gestures are now easier to implement. It provides a declarative API for creating gestures and animations and coupled with Reanimated, it can be used to create complex gestures and animations.

Impact: Smoother gestures

NavigationNavigation works as the backbone of the app and can have a big impact on performance. Thanks to great libraries like React Navigation, navigation is now easier than ever. Adding React Native Screens makes navigation even more performant. Also, opting for react-navigation-native-stack instead of react-navigation-stack can bring some big performance improvements. @react-navigation/native-stack offers native performance as it leverages native APIs UINavigationController on iOS and Fragment on Android. It also offers more native UI features like large titles; though it’s not as customizable as react-navigation-stack.

Moreover, optimizing options in the tab navigator can improve memory usage and frame rate.

Impact: Better performance for navigation

Performance checks in CI/CDPerformance is a continuous process and requires constant monitoring and optimization. As we keep adding more features, it is important to make sure that performance is not compromised. It is important to have performance checks in CI/CD to ensure that performance benchmarks are not broken. Tools like React Native Reassure and Shopify’s React Native Performance are easy to integrate and can be used to run performance checks in CI/CD. These tools can give you a good idea about startup time, memory usage, frame rate, etc. and can be used to set performance benchmarks.

Parting thoughtsPerformance is a marathon and not a sprint. It is a continuous process and requires constant monitoring and optimization. In this article, we discussed some of the tools and techniques that can help you improve the performance of your React Native apps. We will be adding more tools and techniques in the future. If you have any suggestions or feedback, please feel free to add comments below.

The post How to Improve React Native Performance appeared first on Semaphore.

View Details

With the rise of new powerful technologies, everything in the web world is getting streamlined day by day. The emergence of AI technology is one prime example of this. AI offers a new method of performing various tasks with the power of premade algorithms. The arrival of ChatGPT is yet another great statement made by artificial intelligence that has shown how AI can automate writing with minimal human effort.

Currently, ChatGPT is getting a lot of attention due to its great responsive algorithm. It is a new kind of AI tool that responds to almost any question. This means that ChatGPT can interact with humans without feeling robotic. By understanding natural language, it can do various types of tasks just like a human can do in a normal environment. This is one of the core reasons why ChatGPT is being hailed as a new-age innovation in the field of AI technology.

However, there is a flip side, and ChatGPT has also raised many concerns. Many people think that ChatGPT will directly impact human-held jobs. The potential that ChatGPT has shown to respond meaningfully to many types of queries has caused people to worry about future implications.

This article focuses on the impact of ChatGPT on software development. Since the tool has also shown great potential in coding, many people are worried that ChatGPT could take their jobs in the future. Let’s delve into this and see if you really need to worry about ChatGPT taking your job.

What is ChatGPT?ChatGPT is a newly-introduced AI tool that responds to queries with human-like text. It is built primarily to understand natural language questions and prepare answers for them accordingly. Using advanced machine learning algorithms, ChatGPT gives answers that sound convincingly human. This is what makes it stand out from other AI tools.

Developed by OpenAI, ChatGPT has a high-level ability to interact intelligently with natural language. It uses an NLP model to understand queries and generate answers for them. The tool was trained on a large set of text data, meaning that it has a large pool of words that can be used to generate answers for all types of questions. From short text-based paragraphs to long stories, ChatGPT provides answers on a wealth of topics. Not just that, but it also understands different graphic design terms, meaning that it can help you write more complex content.

This is certainly a unique feature that is not seen in any other product currently available. The best part is that it is only going to get better with more advanced AI algorithms in the future.

Important capabilities of ChatGPTChatGPT is not just a tool to generate content. It is a complete system that provides human-like interaction. When it was introduced on the market, people thought that ChatGPT was just another content writing tool. But it has proven them wrong by showcasing stunning capabilities for writing code in several server-side scripting languages.

This ability makes ChatGPT a one-of-a-kind AI tool that can even perform coding for development. The large language models used by ChatGPT enable it to understand programming instructions and produce stunning results . And this is not just limited to one programming language, rather it can accommodate any coding style and language that is out there.

According to a report by Stanford University, ChatGPT has 175 billion parameters and is trained on 570 gigabytes of text. This shows how powerful this AI tool is, as it is capable of performing tons of computations and processing with a large set of text data.

Besides providing answers in English, ChatGPT can interact in other languages as well. This means that you can use ChatGPT to translate sentences from one language to another. From French to Spanish and Arabic to Chinese, without limitations. This is also one of the main reasons why ChatGPT has quickly gained global recognition, and people have proclaimed it the next big innovation of AI.

ChatGPT and software developmentWhen ChatGPT came out on the market in late November 2022, people regarded it as a powerful content generation tool. But quickly, the curtains came off and ChatGPT showed its amazing potential for writing software code as well. This came as a surprise for everyone, because nobody was expecting this ability from this tool. This stunning capability made headlines in the field of software development, giving developers a new tool to add to their repertoire.

Considering its current capabilities, ChatGPT brings a lot of benefits for developers. It can write complete programming code on its own, allowing developers to just sit back and provide the instructions. All they have to do is check the correctness of the code generated at the end, but this is not a difficult task because ChatGPT writes code very well. This certainly represents a next-level evolution, showing how software programming (like the development process) can also be automated.

However, with this great advancement, some valid concerns have also been raised by the development community. Many people fear that this great evolution could replace their jobs in the future. This is not an overestimation, but a very valid point to examine. The capabilities shown by ChatGPT are certainly exemplary, which is why its potential growth in the future and the implications thereof have people concerned.

Is ChatGPT really a threat to software developers?After looking more closely at the great success of ChatGPT and its capabilities, many developers are quite skeptical of whether it is a threat to their future or not. Well, let’s dig a bit deeper into this. Rather than looking at it as an existential threat, let’s focus on the benefits that ChatGPT offers for developers. For starters, let’s first look at some advantages of using ChatGPT for software development. Then, let’s discuss where ChatGPT doesn’t measure up well against human devs, and why software development will always need humans steering the process.

Benefits of ChatGPT for Software DevelopmentChatGPT offers a lot of benefits for software developers. It is a tool that cuts down the need for lengthy manual coding. This is something that has been long needed by the development community, as many people often become frustrated by doing repetitive coding tasks. With the help of ChatGPT, this problem can be easily solved, allowing developers to just sit back and instruct the AI tool to write automated code for them.

With the use of in-depth machine learning algorithms, ChatGPT can handle and manipulate data in bulk. This means that basic programming operations can be easily done with ChatGPT. It will help you to quickly create functions, modules, and so on. And, based on case-specific requirements, you can always edit the results to create the perfect code for any needed software feature.

This is how ChatGPT will help to simplify various coding tasks. It is something that will speed up the development process, enabling developers to code multiple application features quickly.

Major Limitations of ChatGPTChatGPT is undoubtedly a great AI tool that brings various advantages for software development. However, it is still a computer bot and has a few limitations in its system. These boundaries can’t be passed by the AI itself, and can only be resolved by a human. This is one of the core reasons why human developers still have an edge over ChatGPT. There are various jobs in which software developers need to work themselves, because they cannot be done using automated code.

This fact has been proven multiple times, and ChatGPT is not an exception. A software product feature with a critical custom requirement can only be created by human developers. Humans program applications with proper QA checks, which is something that cannot be done by ChatGPT. This is one of the great advantages of manual coding which gives humans an edge over automated platforms like ChatGPT. ChatGPT can do some of the heavy lifting, but delicate tasks and overall direction remain out of its reach.

ConclusionChatGPT is undoubtedly a revolutionary AI tool that will bring huge benefits for software developers. From speedy code generation to simplifying redundant programming tasks, ChatGPT is the solution many in the development community have been looking for.

The rise of AI technology is definitely a great plus for the software development field. And both human developers and tools like ChatGPT can coexist harmoniously.

To assuage the worries of those who fear that ChatGPT and potential future tools like it will take their jobs, one only needs to look at the weaknesses of AI. ChatGPT is an amazing achievement, but it is not the equal of the human brain. Humans will remain essential to the software development process for the foreseeable future and AI advances will help them to be better developers.

The post ChatGPT Writes Code: Will It Replace Software Developers? appeared first on Semaphore.

View Details

Building resilient systems is crucial in ensuring that applications can keep functioning even while facing unexpected failures and disruptions. In fact, designing systems that can bounce back quickly and effectively is essential for long-term success. In this episode, we speak with Software Developer Bart de Water, who will share his expertise on the principles and strategies for creating reliable systems that can withstand potential and even unexpected failures.

Edited transcriptionBart de Water started his career working at a startup straight out of college. Later on, he moved to Montreal and joined the eCommerce platform Shopify to help develop their payment system. After six years of working at Shopify, Bart moved to Thatch, a brand-new application for managing health plan payments.

Bart became an expert in designing resilient systems after experiencing firsthand how easily small issues could grow to the point of taking down whole systems.

Manage timeouts and retries“We were working on building a single sign-on system for the UK government called GOV.UK Verify,” Bart recalls. Upon launch, despite his product working as expected, it was taken down by a third-party backend service.

The service timeout tied the system threads to a 60-second wait; hence, they were as incapable of communicating with this third-party service as with any other request. A timeout sets a maximum time limit for a process or operation to complete: if the process or operation takes longer than the specified time, it will be terminated or canceled automatically by the system. However, in Bart’s anecdote, the long timeout of this service prevented the system from taking care of other critical tasks like sending email confirmation links or 2FA codes. “People were complaining that they couldn’t log in, they couldn’t complete their registration, even though our system was fine, it effectively was down because of this other system being down,” he says.

According to Bart, lowering timeouts can solve issues in which your system resources communicate with external systems whose uptime you have no control over. In this way, it is crucial to set timeouts for every external service your system calls and keep it reasonably low. The same applies to HTTP calls and data structure stores such as Redis. Likewise, Bart recommends keeping an eye open and studying your tools and languages to find and configure their timeouts, as “even internally in your programming language, there might be something like a foot gun waiting for you to stumble upon.

Bart understands that how many retries you should let your service perform is context specific. Real-time processes with real-time requirements, such as card authorizations, can’t take long to retry. In turn, background jobs, such as “to backfill some data from your upstream provider,” taking longer to complete shouldn’t be an issue. In this case, Bart recommends implementing a Fibonacci sequence as a time interval between retries. In this way, it is possible to keep retrying yet without saturating the system: “you don’t care about how fast it’s done, just more that it eventually gets done.”

Idempotency keysBart affirms that “networks are inherently unreliable” to the point that, in some cases, the system can timeout and retry an already processed request. This can be particularly unsafe when processing sensitive data such as credit card payments and can result in duplicate transactions.

To address the issue of unwanted retries and add a layer of resiliency to the system, Bart recommends implementing idempotency keys. These unique and randomly generated keys can singularize and identify each request, preventing a given request from being processed more than once, even if the system sends the same request multiple times.

Understanding queuesBart emphasizes the importance of queues in managing a system’s workload efficiently. He suggests learning about queuing theory and concepts like Little’s law, which can help design systems to match expected demand while remaining cost-effective.

Withal, Bart notes that developers often overlook queue latency in monitoring and diagnosing systems, despite being just as crucial. While waiting five seconds for a page to render is unacceptable, a job queue growing in the background can cause system failures. To provide an example of a queue getting too long, Bart reflects on his experience with the feature launch at the startup. A background job system generated two-factor authentication codes before queuing them for sending. However, by the time the message containing the code was sent, it had already expired. For this reason, Bart suggests setting limits on the queue’s length to prevent similar issues in the future.

Implement circuit breakersCircuit breakers are a design pattern employed to monitor the likelihood of third-party applications halting our system and interrupting communication if detecting signs of malfunctioning. “If you know that something is down, you might as well just stop trying for a little while,” Bart recommends.

Once you have identified that a system is down, you need to acknowledge that it will probably remain down for at least a couple of minutes. Hence, the next step is to “figure out whether your web framework or programming language has a circuit breaker that, once you’ve triggered an X amount of errors, short circuits the next attempt for a certain period.” In this way, you can prevent requests piling up that can cause the system to go down due to the sudden flood of all requests —which is known as the thundering herd problem.

The fundamentals of designing circuit breakers are summarized in Google’s four golden signals for monitoring distributed systems:

  • Latency. How long it takes to process a request and to differentiate between the latency of successful and failed requests.
  • Traffic. The system’s demand in terms of how many requests arrive in its queue and how many available resources the system has.
  • Errors. The rate of requests that fail and why they fail.
  • Saturation. The system’s load at a given present or future time.

Bart also advises being careful when keying circuit breakers to endpoints. He recalls a time at Shopify when, while using a single endpoint for worldwide payments, a significant error rate in one country activated the circuit breaker of the endpoint, hence taking down the payment process service worldwide. To fix this issue, he added the merchant’s country code to the API endpoint host and port and implemented a circuit breaker for each country. As a result, “the next time there was an outage that was confined to a single country’s payment network that did not take down our global payment processing.”

Bart thinks it is essential to learn how to respond to the new ways a system or component can fail to perform its intended function: “There’s going to be new ways that your system is going to fail and that’s fine.” Additionally, to learn from unexpected failures and prevent others, he advises challenging our assumptions when designing our system to identify if these were mistaken or if any blindspots were overlooked.

The bottom lineTo learn more about designing resilient systems, check out Bart’s article “10 Tips for Building Resilient Payment Systems.” You can follow Bart on Twitter, GitHub, and Rubysocial.

The post Bart de Water on Building Robust Resilient Systems appeared first on Semaphore.

View Details

Web development, like most technologies, moves in waves and cycles. Static websites were all we had in the beginning. But pretty soon developers were hacking together the first server-generated sites thanks to Perl and PHP. This was the advance that would eventually kickstart frameworks like Laravel, Django, or Rails.

Mobile devices would come to change how people consume the web. So long server-generated websites, hello client-rendered applications. The next wave brought frameworks that could give users a more app-like experience — without reloads — like React or AngularJS.

But while single-page applications did provide a smoother experience, they have their drawbacks. Namely, longer page loads caused by all the extra JavaScript that has to be parsed and executed. Not to mention all the work it took to optimize for search engines.

Astro is a prime example of the current wave that we have been in since Next.js, taking us full circle: a web framework that combines server and client rendering to get the best of both worlds.

What is Astro?Astro is an open-source framework for generating web applications on top of popular UI frameworks like React, Preact, Vue, or Svelte. An Astro page is composed of several independent components. To improve load times, Astro strips away all JavaScript and prerenders pages in the server unless developers mark a component as interactive, in which case Astro will send the minimum amount of JavaScript required for interactivity.

Astro favors generating static and server-rendered sites without JavaScript in the browser. Developers can opt-in to client-side rendering for given routes or parts of the page.Thanks to this strategy, Astro pages load quickly, as no JavaScript needs to execute for the first render. In a process called hydration, Astro will “pour” JavaScript into components to make them dynamic.

Why Astro?Astro stands out from the other web frameworks in many ways:

  • Speed: Astro is statically and server-rendered when possible, making it an excellent fit for content-first, SEO-friendly websites like blogs, courseware, or marketing sites.
  • UI agnostic: unlike Next.js, which only works with React, or Nuxt.js–which requires Vue expertise, Astro doesn’t force any UI framework on you. You can use none, roll your own, or use any of the many supported integrations.
  • Themes: Astro has an impressive array of ready-made themes and templates to get started quickly.
  • Easy to pick up: you don’t need to learn React or Vue to get started with Astro. Its templating engine looks like plain HTML, and the code is cleanly separated from the presentation. You can pick an empty project and build it up at your own pace.
  • Batteries included: Astro comes with the features we expect from a modern JavaScript/TypeScript framework like top-level awaits, Markdown and MDX support, and ESM includes.
  • Islands: the island architecture allows us to mix static content, server-rendered, and client-rendered components on the same page without conflicts, allowing us to build interactive sites with minimal overhead. We can even mix different frameworks in the same page, giving us yet another way of building microfrontends. We’ll see more about how islands work in a bit.

Getting started with AstroTo get started with Astro, install Node version 16.12.0 or higher and run the following command. Follow the on-screen wizard and choose to create an empty project when prompted:

$ npm create astro@latest astro v1.9.1 Launch sequence initiated.✔ Where would you like to create your new project? … awesome-website✔ How would you like to setup your new project? › an empty project✔ Template copied!✔ Would you like to install npm dependencies? (recommended) … yes✔ Packages installed!✔ Would you like to initialize a new git repository? (optional) … yes✔ Git repository created!✔ How would you like to setup TypeScript? › Relaxed✔ TypeScript settings applied! next Liftoff confirmed. Explore your project! Next, you can start the website in developer mode by entering the directory of the project you just created and running: “npm run dev” and visiting http://localhost:3000.

Pages and routesThe interesting stuff in Astro happens inside the src folder. Checking what’s there, we see a single directory called pages with an index.astro file.

Astro pages are a mix of HTML, Javascript, or TypeScript. This is the default index.astro:

------<html lang="en"> <head> <meta charset="utf-8" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <meta name="viewport" content="width=device-width" /> <meta name="generator" content={Astro.generator} /> <title>Astro</title> </head> <body> <h1>Astro</h1> </body></html> You may have noticed that Astro files start with fences delineated with ---. Whatever code we put inside these fences is executed on the server and prerendered before serving the page.

Below the frontmatter, we find the content for this route, an augmented HTML form that allows for variables. We can, for example, define a variable in the frontmatter and use it in HTML like this:

---// src/pages/index.astroconst title = "Astro";---<html lang="en"> <head> <meta charset="utf-8" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <meta name="viewport" content="width=device-width" /> <meta name="generator" content={Astro.generator} /> <title>{title}</title> </head> <body> <h1>{title}</h1> </body></html> Astro uses a file-based routing, so every file in the pages folder is mapped to a route in the website. For example, if we create a file called greetings.astro, we should see its contents at http://localhost:3000/greetings.

---const greeting = "Hello, world!";---<html lang="en"> <head> <meta charset="utf-8" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <meta name="viewport" content="width=device-width" /> <meta name="generator" content={Astro.generator} /> <title>Astro</title> </head> <body> <h1>{greeting}</h1> </body></html> In addition to .astro files, Astro can parse Markdown, MDX, JSX JavaScript, and TypeScript files. For instance, if we want to write a blog post in Markdown, we create a post.md file under the pages folder. Visiting the route will make Astro convert it into HTML on the fly:

---title: 'Learning Astro'pubDate: 2023-01-10description: 'A post written in Markdown.'maauthor: 'Tommy'---# Learning AstroThis Markdown file should be rendered as HTML when I visit http://localhost:3000/post ComponentsAstro components are *.astro files with reusable code and HTML. We can use components to write elements like headers, footers, navigation bars, buttons, and forms — anything that can be expressed as HTML can constitute a component.

Let’s create our first component in src/components/Header.astro:

---// src/components/Header.astro---<meta charset="utf-8" /><link rel="icon" type="image/svg+xml" href="/favicon.svg" /><meta name="viewport" content="width=device-width" /><meta name="generator" <title>Astro</title> Once defined, we can import it to any page (or other components) and use it like this:

---import Header from "../components/Header.astro";---<html lang="en"> <head> <Header /> </head> <body> </body></html> Astro components are no different from pages. Any code defined between fences is executed on the server. JavaScript is stripped out before sending the content to the browser.

LayoutsLayouts are used to refine reusable UI structures. They are technically components, so the syntax stays the same.

Let’s replace the content of index.astro with a layout:

---// src/pages/index.astroimport SiteLayout from "../layouts/SiteLayout.astro";---<SiteLayout></SiteLayout> As you can see, layouts are stored in the src/layouts folder by convention.

Layouts, like components, can include other components. Here we have extracted the structure in index.astro and added a Footer component:

---// src/layouts/SiteLayout.astroimport Header from "../components/Header.astro";import Footer from "../components/Footer.astro";---<html lang="en"> <head> <Header /> </head> <body> <Footer /> </body></html> Props and slotsSo far, our website has been completely static. In order to pass data between pages and components, we need to understand how props and slots work.

Components and layouts can define and accept props (short for properties) via the global Astro.props. Values passed through props are accessible to the component before rendering.

We can read props in our component like this:

---// src/components/Header.astroconst { title } = Astro.props;---<meta charset="utf-8" /><link rel="icon" type="image/svg+xml" href="/favicon.svg" /><meta name="viewport" content="width=device-width" /><meta name="generator" <title>{title}</title> The value of title can be supplied when the component is called, which in the following example happens through the SiteLayout layout.

---// src/layouts/SiteLayout.astroimport Header from "../components/Header.astro";import Footer from "../components/Footer.astro";---<html lang="en"> <head> <Header title = "Welcome my Astro Blog!" /> </head> <body> <Footer /> </body></html> ⚠️ Note that you need spaces surrounding the equal sign, i.e. title="Hello" is NOT correct. Instead, it should be: title = "Hello".

Slot elements create placeholders for content to be injected later. To see how it works, we can add a <slot /> element in src/layouts/SiteLayout.astro:

---// src/layouts/SiteLayout.astroimport Header from "../components/Header.astro";import Footer from "../components/Footer.astro";---<html lang="en"> <head> <Header title = "Welcome my Astro Blog!" /> </head> <body> <slot /> <Footer /> </body></html> Now the HTML inside <SiteLayout> is injected into the point where the slot is located.

---// src/pages/index.astroimport SiteLayout from "../layouts/SiteLayout.astro";---<SiteLayout> <p>This content is rendered in the slot</p></SiteLayout> Nesting components, layouts, props, and slots give us the flexibility to create reusable UI files across the website.

Islands and hydrationUp to this point, we have not shipped any JavaScript to the user; everything is prerendered and served as pure HTML+CSS. How do we send make Astro send JavaScript to the browser? For that, we need to understand the islands architecture.

The island architecture pattern aims to reduce the amount of JavaScript needed browser-side. Less JavaScript means less data sent and less computational power required on the user’s device. An island is an autonomous component that bundles HTML, CSS, and — optionally — JavaScript. In the islands pattern, a page is made up of several independent islands.

The islands architecture allows combining static HTML, server-rendered content, and interactive client-rendered components on one page without conflicts.Each island is prerendered, so there is no interactivity right after the page loads. Once the initial page is ready, the islands are converted into interactive content in a process called hydration. Hydration is a technique that converts static content delivered via static hosting or server-side rendering into a dynamic page by attaching event handlers to the HTML elements.

How hydration works. The first contentful paint happens right after all the HTML has been sent to the browser, which typically happens very quickly. The browser then requests and executes all the client-side JavaScript needed to make the page fully interactive.Using IslandsAn example showing how Astro implements islands is in order. Let’s first add a UI integration to the project. The following command installs @astrojs/preact and preact.

$ npx astro add preact Let’s create a simple button to test the integration:

// src/components/MyButton.jsxexport default function MyButton() {const clicked = () => { console.log('Hello!') };return ( <div> <button style={{ color: 'purple' }} onClick={clicked}>Click me</button> </div> )} As usual, Astro will try to strip away any JavaScript. So nothing would happen if we instantiated the component with <MyButton />. We need to tell Astro that we want this component treated as an island and hydrated accordingly by adding the template directive client:load:

---import MyButton from "../components/MyButton.jsx";---<html lang="en"> <body> <MyButton client:load /> </body></html> Clicking the button should print “Hello!” in the browser console.

The client directive caused Astro to hydrate the component. There are five levels of hydration with different priorities:

  • client:load hydrates the component as soon as possible.
  • client:idle hydrates the component when the page is done loading. Useful for low-priority components that don’t need immediate interactivity.
  • client:visible={string} hydrates the component as soon as it appears in the viewport.
  • client:media={string} takes a CSS query as an argument and loads the component as soon as it’s fulfilled.
  • client:only skips HTML rendering entirely and renders the component in-browser.

Deploying Astro application with CI/CDBeing a Node application, setting up an Astro build with CI/CD is very straightforward. We only need a couple of jobs.

A possible continuous integration pipeline for Astro.The first job runs npm ci to populate node_modules. We use sem-version to select a current Node version.

checkoutsem-version node 19.4npm cicache store The second job runs npm run build and stores the built application (located in the dist folder) as a workflow artifact.

checkoutsem-version node 19.4cache restorenpm run buildartifact push workflow dist/ Once the site is built, we can configure continuous deployment.

Deployment targetsDepending on the nature of the web application, Astro can be deployed as a static site à la Hugo or Gatsby, in which case we only need something as simple as an S3 bucket or GitHub pages, or as a full-fledged server-side rendered (SSR) application, where we need JavaScript- or TypeScript-enabled endpoints.

Astro has built-in support for various popular deployment targets such as Netlify, Firebase, Vercel, and Deno. Some only support either SSR or static hosting, while others can do both.

Once we have chosen our deployment method, we can add a continuous deployment pipeline to automatically deploy the website on every change.

Here’s an example deployment pipeline that targets a Netlify static site.

The complete CI/CD workflow with continuous deployment to Netlify.For reference, the job commands are as follows. This supposes that we have already obtained an API token and saved it as a secret on Semaphore with the env variables NETLIFY_TOKEN and NETLIFY_SITE.

checkoutartifact pull workflow distnpm install -g netlify-clinetlify deploy --dir=dist --prod --auth $NETLIFY_TOKEN --site $NETLIFY_SITE ConclusionThe popularity of this project has been nothing short of astronomical: in the first five months since the Astro 1.0 release, the project has amassed over 25,000 stars on GitHub. The success of Astro is not accidental. The Astro team has made a web framework that offers great ergonomics for developers and fast-loading websites for users, even if they are on low-power devices or slow connections.

Thanks for reading, and happy building!

The post Getting Started With Astro appeared first on Semaphore.

View Details

A microservices architecture consists of several independent backend services that communicate with each other through APIs. Given the popularity of the microservices approach, chances are that your backend needs to make several API calls to receive data from and send data to other backends.

Making an HTTP request to call an API takes only a few lines of code. However, you likely need to call the same API in different places. This implies repeating the same logic over and over again. Such an approach is cumbersome and leads to code duplication. Just think about when the endpoint of an API changes. In this common scenario, you will have to update the code at all points where the API is called.

Now, imagine that your backend software architecture had a layer to contain everything required to call all APIs. Centralizing all the code to make API calls in one place would provide several benefits and help mitigate the negative aspects mentioned earlier. This is what an API layer is all about!

Let’s figure out what an API layer is, understand why your backend app should have one, and learn how to integrate an API layer into your Spring Boot backend.

What is an API Layer?An API layer is the part of the backend Application that encapsulates all the Programming logic required to receive and send data through an Interface (API). In other words, all external API calls a backend application relies on pass through this architectural layer. Keep in mind that this is nothing new, and you can take advantage of an API Layer also in a frontend application. Find out more about how to build an API Layer in React.

In a Spring Boot application, all the logic required to implement the API layer should be placed in an api package. Then, you can use the following format to name the Java class files contained in that package:

<ExternalService>API.java This Java file should expose all API calls related to a particular external microservice as functions. Thus, the api package will consist of several classes. Each of these files will collect all APIs related to the service identified with the <ExternalService> qualifier. This will also make finding APIs of the same type more intuitive.

Keep in mind that a backend API layer usually also requires some utility classes. These files do not follow the naming convention presented above. So, here is what an API layer in a Spring Boot application may look like:

api├── utils│ └── APIHandler.java │├── APILayer.java...├── AuthorizationAPI.java...├── CmsAPI.java...└── SemaphoreAPI.java As you can see in the file diagram above, the API layer groups together all the APIs of external services that your backend application uses.

In particular, let’s focus on the APILayer.java file. This is a special file that encapsulates all the <ExternalService>API.java files as follows:

package com.apilayer.service.api;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;@Servicepublic class APILayer { private final AuthorizationAPI authorizationAPI; private final CmsAPI cmsAPI; private final SemaphoreAPI semaphoreAPI; // other <ExternalService>API ...@Component APILayer( @Autowired AuthorizationAPI authorizationAPI, @Autowired CmsAPI cmsAPI, @Autowired SemaphoreAPI semaphoreAPI // ... ) { this.authorizationAPI = authorizationAPI; this.cmsAPI = authorizationAPI; this.semaphoreAPI = semaphoreAPI; // ... } public AuthorizationAPI getAuthorizationAPI() { return authorizationAPI; } public SemaphoreAPI getSemaphoreAPI() { return semaphoreAPI; } public CmsAPI getCmsAPI() { return cmsAPI; } // other getters...}

APILayer is annotated with @Component because it is generally contained in the service layer. The reason for this is that retrieving and writing data via API is part of the business logic, which should be defined in the service layer.

So, if your Spring Boot backend follows a multi-tier architecture with a package for each layer, the api package should be placed inside the service layer:

com.your-app├── controller│ └── ... ├── dao│ └── ...├── data│ └── ...├── dto│ └── ...├── model│ └── ......└── service ├── api │ ├── utils │ └── ... └── ... Note the api folder under the service directory. The API layer can be considered a specialized sub-layer of the service layer.

Thanks to APILayer, importing the entire API layer requires only one @Autowired variable. Specifically, you can now call an API in a @Service class as follows:

package com.apilayer.service;import com.apilayer.service.api.APILayer;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import java.util.List;// ...@Servicepublic class ArticleService { private final APILayer apiLayer; // registering the APILayer bean // via constructor dependency injection public ArticleService(@Autowired APILayer apiLayer) { this.apiLayer = apiLayer; } public List<Article> getAllArticles( ) { // retrieving some data from an external CMS // through an API call List<Article> articles = apiLayer.getCmsAPI().getAllArticles(); // remaining business logic... return articles; } // ...} This example shows how the API layer simplifies the process of sending and receiving data via API. In detail, the API layer provides functions that encapsulate all the logic required for performing API calls.

Let’s now dig into the benefits an API layer can bring to your backend architecture.

Why Your Spring Boot Application Should Have an API LayerAn API layer offers several advantages to the architecture of a Spring Boot application. Let’s take a look at the three most important reasons why you should use it.

1. Avoiding Code DuplicationIn the microservices architecture, backend applications use APIs to communicate with each other. So, your backend is likely to make a lot of API requests. In a Spring Boot application with a traditional architecture, this means repeating the lines of code shown below throughout your backend codebase:

// verifying if an authorization token// is valid by calling an API// exposed by the authentication microserviceHttpHeaders httpHeaders = new HttpHeaders();httpHeaders.add(HttpHeaders.AUTHORIZATION, token);boolean isValid = return restTemplate.exchange( "https://your-auth-backend.com/api/v1/verifyToken", HttpMethod.GET, new HttpEntity<>(null, httpHeaders), Boolean.class); The example above shows how to call an API in Spring Boot with RestTemplate, the default Spring Boot Web HTTP client. Specifically, it demonstrates how to make an authentication API request. Since all authenticated APIs in your backend will rely on this external API call, you will have to replicate this logic multiple times. This will lead to duplication, which increases the lines of code in the codebase and thus the possibility of introducing errors and bugs. Specifically, even a single API called incorrectly could cause your application to crash, and you want to avoid this. Plus, the number of APIs a backend depends on is generally very limited. So, duplicating some logic to call the same APIs seems inevitable with this approach.

Now, consider centralizing all the logic behind your API calls in functions exposed by an API layer. Performing an API call would take a single line of code, the function call. Also, when an API endpoint changes, you would only need to update a single function. Without the API layer, you would have to find and change every API call made throughout the code. A huge difference!

Therefore, an API layer makes the Spring Boot backend more modern, easier to maintain, and cleaner, without the same lines of code repeated again and again.

2. Simplifying Integration of Third-Party ServicesToday’s most successful services are based on a composable architecture. This involves integrating best-in-class online services into a single application. After all, no one wants to reinvent the wheel, and relying on successful products makes everything easier.

You can apply the same logic to a backend application, especially considering that most SaaS (Software as a Service) services are now API-based. This means that you can get access to their data and functionality via API.

With an API layer, you already have the infrastructure in place to handle API calls. In particular, integrating a new third-party service into your backend simply involves defining a new <ExternalService>API.java file. To make things even simpler, you can also configure a custom RestTemplate instance as follows:

// building a RestTemplate instance// with custom default headersRestTemplate semaphoreRestTemplate = new RestTemplateBuilder() .defaultHeader("Authorization", "Token: <YOUR_SEMPAHORE_API_KEY>") .build(); For example, you can use this instance of RestTemplate in your SemaphoreAPI.java file. Every API call made with that instance will automatically have the Authorization HTTP header set to the desired value.

Thanks to the API layer, integrating external services requires only a few lines of code. All the logic is centralized in one file and exposed through functions. so switching from one service to another is simple. You only have to update the code inside each function in the corresponding <ExternalService>API.java file. This saves you from major refactoring.

3. Making It Easier to Add Retry Logic on API FailureNo API-based SaaS offers 100% uptime. In addition, some of these external services may be error-prone, especially during updates. This means that third-party services are no strangers to server-side HTTP 5xx errors, especially the 503 Service Unavailable variety.

When a third-party service responds with an error, your backend may fail as a result. Therefore, errors returned by any external service used by the backend are also your problem. At the same time, third-party services base their success on the effectiveness and availability of their features. So, these errors must be temporary. This is why retrying HTTP requests in case of specific errors is so important.

Thanks to Spring Retry, you can effortlessly integrate retry logic into your Spring Boot app. All you have to do is decorate the API layer methods with the Spring Retry @Retryable annotation. These methods will then be executed up to three times, with a delay of one second between each attempt.

Adding an API Layer to a Spring Boot AppYou now know what an API Layer is and what benefits it can bring to a Spring Boot application. It is time to learn how to implement one!

PrerequisitesFirst, you need a Spring Boot Web application in Java. You can create one with Spring Initializr as follows:

You are free to choose Gradle or Maven as the build tool, but you must select Java as the programming language and add Spring Web as a dependency. This way, Spring Initializer will automatically add spring-boot-starter-web to your project’s dependencies. Note that Spring Web includes RestTemplate– the HTTP client used by the Spring Boot API Layer to perform HTTP requests.

Implementing an API LayerLet’s see how you can add an API layer to a Spring Boot Web app. The first thing you need to do is create a utility class to make calling APIs easier.

In the utils package, you can define an APIHandler.java class as shown below:

package com.apilayer.service.api.utils;import org.springframework.boot.web.client.RestTemplateBuilder;import org.springframework.http.HttpEntity;import org.springframework.http.HttpHeaders;import org.springframework.http.HttpMethod;import org.springframework.http.ResponseEntity;import org.springframework.stereotype.Component;import org.springframework.web.client.RestClientException;import org.springframework.web.client.RestTemplate;@Componentpublic class APIHandler { private final RestTemplate restTemplate; public APIHandler(RestTemplateBuilder restTemplateBuilder) { // initializing a RestTemplate instance // to perform API calls this.restTemplate = restTemplateBuilder.build(); } // defining a utility function // to perform API calls public <T, R> ResponseEntity<R> callAPI( String apiEndpoint, HttpMethod httpMethod, HttpHeaders httpHeaders, T requestBody, Class<R> classToConvertBodyTo ) { // setting the HTTP headers and HTTP body (if present) HttpEntity<T> requestEntity = new HttpEntity<>(requestBody, httpHeaders); try { // performing the HTTP request return restTemplate.exchange( apiEndpoint, httpMethod, requestEntity, classToConvertBodyTo ); } catch (RestClientException e) { // logging errors when the HTTP request fails // or there is an error in decoding the response e.printStackTrace(); // other default behavior... // (e.g. register the error in an APM service, ...) throw e; } }} As you can see, this class initializes an instance of RestTemplate in the constructor with RestTemplateBuilder. Then, it uses that instance in callAPI() to make HTTP requests via the RestTemplate exchange() method.

The advantage of centralizing API call logic in an APIHandler utility class is that you can define custom behavior for all your calls directly here. For example, you can log errors in callAPI() when the exchange() method raises a RestClientException. This is particularly useful for debugging purposes.

Similarly, you can add custom global configs to the RestTemplate instance when initializing it in the constructor. For example, you can add a custom HTTP header to the request performed with that RestTemplate instance using the defaultHeader()method:

// add a custom HTTP header to the RestTemplate instanceRestTemplate scrapingRestTemplate = new RestTemplateBuilder() .defaultHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36") .build(); Therefore, as different third-party services typically have different requirements in terms of headers or procedures for calling their APIs, you may need to create many APIHandlers. Each will have dedicated utility functions that rely on a specifically configured RestTemplate instance.

Now, let’s see how to use APIHandler to build an <ExternalService>API.java API layer file:

package com.apilayer.service.api;import com.apilayer.data.pokeapi.NamedApiResourceList;import com.apilayer.service.api.utils.APIHandler;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.http.HttpMethod;import org.springframework.http.ResponseEntity;import org.springframework.stereotype.Component;import org.springframework.web.util.UriComponentsBuilder;import com.apilayer.data.User;@Componentpublic class AuthorizationAPI { private final APIHandler apiHandler; private final String BASE_URL = "https://your-auth-backend.com/api/v1/users"; AuthorizationAPI(@Autowired APIHandler apiHandler) { this.apiHandler = apiHandler; } public User getUserDataByToken(String token) { // retrieving a single User by token ResponseEntity<User> response = apiHandler.callAPI( String.format(BASE_URL + "?token=%s", token), HttpMethod.GET, null, null, User.class ); return response.getBody(); } public bool isAuthenticated(String token) { // inserting the token string in the // HTTP "Authorization" header HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.add(HttpHeaders.AUTHORIZATION, token); // verifying whether a user is authenticated // by checking his authorization token ResponseEntity<Boolean> response = apiHandler.callAPI( BASE_URL, HttpMethod.GET, httpHeaders, null, Boolean.class ); return response.getBody(); } // other authorization API functions...} Your backend may rely on different APIs from your authentication backend. Each of these APIs will correspond to an API layer function, which contains all the logic to call the API encapsulated in a few lines of code. Do not forget that this is just one <ExternalService>API.java file, and your Spring Boot app might require many more API layer files.

Integrating the Retry LogicAs explained earlier, you now need Spring Retry. Since Spring Retry annotations require the Spring AOP dependency, you need to install two packages. Let’s add both spring-retry and spring-boot-starter-aop to your project’s dependencies.

If you are a Maven user, add the following lines inside the <dependencies> tag of your pom.xml file:

<dependency> <groupId>org.springframework.retry</groupId> <artifactId>spring-retry</artifactId> <version>2.0.0</version></dependency><dependency> <groupId>org.springframework</groupId> <artifactId>spring-boot-starter-aop</artifactId> <version>3.0.2</version></dependency> Otherwise, if you are a Gradle user, add the two lines below in the dependencies object:

implementation "org.springframework.retry:spring-retry:2.0.0"implementation "org.springframework.boot:spring-boot-starter-aop:3.0.2" Enable Spring Retry by decorating your @SpringBootApplication class or one of your @Configuration classes with the @EnableRetry annotation:

You can now use @Retryable to implement retry logic in the APIHandler callAPI() method as shown below:package com.apilayer;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.retry.annotation.EnableRetry;@SpringBootApplication@EnableRetrypublic class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); }} You can now use @Retryableto implement retry logic in the APIHandler callAPI() method as shown below:

@Retryable( // attempting up to 10 times maxAttempts = 10, // with a delay of 2 seconds between attempts backoff = @Backoff(delay = 2000), // when the Http5xxException is raised by the method retryFor = Http5xxException.class)public <T, R> ResponseEntity<R> callAPIWithRetryLogic( String apiEndpoint, HttpMethod httpMethod, HttpHeaders headers, T body, Class<R> classToConvertBodyTo) { // setting the HTTP headers and HTTP body (if present) HttpEntity<T> requestEntity = new HttpEntity<>(body, headers); try { // trying to perform the request return restTemplate.exchange( apiEndpoint, httpMethod, requestEntity, classToConvertBodyTo ); } catch (RestClientResponseException e) { // in case of a 5xx error, throwing a custom // exception that triggers the retry logic if (e.getStatusCode().is5xxServerError()) { throw new Http5xxException(e.getMessage()); } // some default behavior... e.printStackTrace(); throw e; } catch (RestClientException e) { // logging errors when the HTTP request fails // or there is an error in decoding the response e.printStackTrace(); // other default behavior... // (e.g. register the error in an APM service, ...) throw e; }} Now, when the server returns a 5xx HTTP error, the backend will try up to ten times to execute the API call. Note that retrying on 4xx HTTP errors does not make sense. This is because 4xx errors are client errors, and retrying would result in the same error over and over again.

In detail, callAPIWithRetryLogic() retries on Http5xxExceptions. This is a custom error exception that you can define as follows:

package com.apilayer.exceptions;public class Http5xxException extends RuntimeException { public Http5xxException(String message) { super(message); }} Note that not all API calls need repeat logic. For this reason, you should define two callAPI()-like functions in APIHandler and annotate only one with @Retryable. You can name these two functions callAPIOnce() and callAPIWithRetry(). To make APIHandler more elegant and avoid repeating code, you should define a private function callAPI() containing the logic to call an API and make the two functions use it as done in the demo codebase.

Furthermore, you should define two callAPI()-like functions in APIHandler and annotate only one with @Retryable.

Well done! You just learned how to implement an API layer in Spring Boot with RestTemplate and Spring Retry!

API Layer in ActionLet’s now see the API layer in action in a Spring Boot demo application. As a third-party sample service, the application uses the free-to-use PokeAPI service. Also, it relies on the Java API models from the pokeapi-reactor repo.

Clone the GitHub repository supporting this article and launch the demo app locally by running the following commands:

git clone https://github.com/Tonel/api-layer-spring-bootcd api-layer-spring-boot./gradlew build./gradlew bootRun You can now call the /v1/getPokemons endpoint with the curl command below:

curl -H "Accept: application/json" -H "Content-Type: application/json" -X GET "http://localhost:8080/api/v1/pokemon/getBlastoise" Alternatively, visit http://localhost:8080/api/v1/pokemon/getBlastoise in your browser.

In both cases, you should get the following result:

{ "id": 9, "name": "blastoise", "image": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/9.png", "height": "160 cm", "weight": "85 kg"} This is nothing more than a transformed version of some data retrieved from a PokeAPI API call!

ConclusionIn this tutorial, you learned what a backend API layer is, its essential benefits in the microservices world, and how to integrate it into a Spring Boot Web application. The API layer is nothing more than a set of components that provide complete functionality for sending and receiving data via API calls. This allows you to consolidate API logic into a single layer of the Spring Boot application architecture, making the entire codebase easier to maintain and more reliable, reducing the possibility for human error . As demonstrated in a complete example, implementing an API layer in Spring Boot is simple and takes only a few minutes.

The post Why You Need an API Layer in Your Spring Boot App and How to Implement It appeared first on Semaphore.

View Details

New businesses that master software delivery can reach valuations of 1 billion in a matter of months, disrupting established ecosystems. To do this, they have to have the capability to deliver software with quality and speed enabling them to rapidly scale valuable offerings based on experimentation. In that context, existing businesses are pressured to compete at the same pace or risk falling behind.

The rise of Agile methodologies and DevOps practices have improved the capability to deliver in faster cycles by leveraging techniques like Continuous Integration and Continuous Delivery (CI/CD). The main challenge with such practices is to ensure that quality requirements are met, as faster iterations increase the risk of defects and the accumulation of technical debt.

While the latest technologies play a critical role in accelerating the delivery of valuable software, efficient and effective software development processes are at the core of streamlined software activities. The development team must be able to perform rapid code changes and deliver them in small and reliable increments to support business goals.

Trunk-based development is a branching practice, identified as a core capability in the DORA research program, for elite performers delivering multiple times per day. This article compares trunk-based development with other branching strategies and how to implement it in your organization to deliver software with quality and speed.

What is trunk-based development?Code versioning, for example, can quickly become a nightmare with an accumulation of small decisions and defects that pile up. When developers work in their own branch with a local version, the effort to merge multiple changes made in the meantime can take hours, if not days. All that time lost merging issues and the accidental complexity is a waste of effort for the company.

Trunk-based development is a software development methodology that focuses on keeping a single source of truth for the code repository, often referred to as the “trunk”, or “main” in the Git ecosystem. One important point is that this master software lane must always be considered as ready for deployment to production, without depending on other branches.

Trunk-based development is based on the following ingredients:

  • Single source of truth in the “trunk” branch containing the latest code version that must be deployable to production.
  • Short-lived branches used to continuously contribute to the central source of truth and work on an up-to-date version to reduce merge risks and defects.
  • Small and frequent commits merging all code changes, even if features are not fully finished, to continuously ensure flawless integration with the rest of the codebase.
  • Frequent communication between development teams as they often touch similar modules while performing an increased rate of code changes.
  • Continuous integration and delivery to keep fast cycles of deployment as the whole team is collaborating in the same place.
  • Automated testing to support fast feedback on code changes while continuously iterating through frequent commits and production deployments.
  • Feature flags to keep the flexibility to activate or deactivate features without blocking the flow of iteration that is always moving forward.

The Lean Principle of limiting the work-in-progress is central to trunk-based development, helping to reduce waste and the number of defects on the production line. In software, its application means that small code changes are very frequently integrated into the main line and incorporated into the releasable version once they pass the quality requirements.

This paradigm encourages developers to commit their code regularly, resolving conflicts early on rather than letting them accumulate over time. By reducing the number of branches with a single source of truth with the latest code version, trunk-based development helps reduce the risk of merge conflicts, improves code quality, and accelerates software delivery.

Trunk-based development is not dependent on software architecture like modular monoliths or microservices—yet, it requires an alignment with the software system. For example, trunk-based development enforces frequent commits that must be reviewed quickly to keep up the fast flow of iteration, and is supported by practices like a release train and feature flags.

How trunk-based development differs from other practicesGitflow, GitHub Flow, or Feature Branch structure team collaboration around different branches based on workflows like development versions, release, or hotfix. While developers feel safe working in a given branch, the release process is painful due to complex merging processes, slow testing, and multiple versions to manage at any given point in time.

Trunk-based development is different from other branching models with just-in-time integration and release of code changes on a single branch. In this model, software delivery constraints cannot be hidden as pending changes, making it the best way to streamline the software pipeline.

The key differences of trunk-based development lie in:

  1. Number of branches: a single branch is used, whereas other branching models use multiple long-lived branches, such as development, feature, and release branches with different lifecycles.
  2. Branching and merging: many short-lived branches that are quickly merged back into the trunk are used, whereas other branching models use long-lived branches merged back at specific points in time, and with complex rebase scenarios.
  3. Collaboration and code sharing: developers are encouraged to commit code changes multiple times per day, and to resolve any conflicts early on, making it easier for teams to collaborate, while other practices support longer periods without merges.
  4. Code quality and delivery: helps to reduce the risk of merge conflicts, and accelerate the delivery of small software increments, while alternatives lead to more complex merging procedures, with more code conflicts and less frequent delivery.

The choice of branching model depends on the specific needs of the team and context of the organization and its alignment with the development team. Google, Spotify, Etsy, Netflix are popular examples of organizations applying the practice, but other branching models may be more suitable for teams with a different organization and culture.

Furthermore, experience matters with trunk-based development. The State of DevOps Report (page 31) identified that new developers tend to make more mistakes and have lower performance, whereas developers with 16+ years of experience take full benefit of trunk-based development and perform better overall.

How to implement trunk-based developmentTrunk-based development is like the cherry on the cake, as it requires multiple software practices to support its performance. Its implementation requires a gradual approach for developers to have the time to incorporate new practices in their work routines, and also for the team to grow in maturity with this paradigm.

It all starts by making it clear that the trunk or main branch is the only one considered as the source of truth for team collaboration. The following 4 steps support the implementation of mature trunk-based development practices to effectively harness its benefits. The key point is to align the software’s technical and organizational systems to work in concert.

  1. Ad-hoc/initial maturityThe first level of implementation focuses on convincing the team of the benefits by making them understand the differences from other models they may be used to, and making small yet important changes in their working routines, i.e. to code and review more frequently.

The actions to drive are:

  • Educating the team on the benefits of trunk-based development and how it differs from traditional branching models.
  • Encouraging small changes and coaching the team to perform incremental changes to the codebase rather than large, monolithic changes.
  • Setting up a synchronous code review process and making it systematic so that all changes are reviewed by at least one other developer before they are merged.

  • Repeatable maturityA team with an accelerated rhythm of iteration needs specific elements of software automation and environments to keep changing code frequently in small increments with a good degree of confidence.

The following practices must be implemented for this to work:

  • Continuous Integration and Continuous Delivery (CI/CD) tools to automate the build, test, versioning, and deployment of code.
  • Setting up testing with a staging environment where changes can be tested before they are deployed to production, and where you will build your test automation infrastructure.
  • Using feature flags that enable/disable new features in production to let developers commit unfinished work and be able to deactivate a feature if needed.

  • Managed maturityOnce a team masters the repeatable process of trunk-based development, the focus shifts to make the process more robust and scalable over-time. That’s where a clear release process that can be improved based on outcomes becomes necessary.

These elements become necessary at that stage:

  • Establishing a release train process that includes testing, staging, and production environments ensuring software versioning with fixed deployment constraints.
  • Setting up a rollback plan in case of issues with a release, as an emergency backup plan.
  • Measuring and monitoring key metrics such as code quality, build and deploy times, and rollback success rates to continuously improve the process.

  • Scalable maturityThe various parts of the process must be delegated to dedicated roles when the codebase reaches a certain size. This final stage consists of supporting a continuous flow of changes with practices requiring an advanced degree of maturity.

The main practices relevant for this stage are:

  • Establishing a platform team responsible for managing the CI/CD pipeline and ensuring that it is always running smoothly, and starting to provide advanced services.
  • Using advanced testing techniques such as chaos engineering and continuous monitoring to proactively identify and fix potential issues.
  • Continuously optimizing the process using data and feedback from the team to make improvements based on identified limiting factors.

Which technologies support trunk-based development?The technology foundations of trunk-based development are similar to other branching strategies as the code still needs versioning, automated build, deployment and testing stages. The key differences for the trunk-based development tooling is to support a faster flow of iteration, requiring advanced capabilities.

The core tooling of trunk-based development is based on:

  1. Version Control to manage the continuous versioning and keep the latest version in the shared codebase that can supported by tools like Git and Git version.
  2. Continuous Integration to support the continuous build pipeline with unit test execution based on the multiple commits to the trunk. This can be achieved via Public Cloud providers like Google, Amazon, or AWS, or dedicated platforms like Bazel or Semaphore.
  3. Continuous Delivery enables the deployment of the same artifact in multiple environments as well as providing progressive delivery capabilities. The tooling can be the same as for the CI, or a platform like Semaphore, GitLab, GitHub, or Harness.
  4. Automated Testing will provide fast feedback loops so that the team can move forward quickly with the proposed changes. The key typologies of testing to start with are to run unitary tests early on as part of the CI build, to then complement them with non-regression tests on the functional and non-functional parts like security and performance. Unit testing tools are available for each programming language, whereas the other layers of testing require specific tooling like Cypress, Katalon, or Cerberus Testing.
  5. Feature flags are critical for the team to perform multiple commits per day on unfinished work and progressively deploy finished features. Tools can be available as part of CD, but can also be done via configuration or specialized solutions like LaunchDarkly or Featureflags.io.

This tooling will support the progressive maturity of trunk-based development that needs to evolve to support the scalability of the practice. The increased flow of developers committing multiple times per day to the same codebase will drastically increase the number of builds, tests and deployments to be performed in a given timeframe.

As a result, if the integrated tooling takes too much time to perform the cycle of build to deployment, code changes will start to accumulate, increasing the work-in-progress and associated risks of defects, as is often the case with other branching strategies. Specific parts of the system require specific attention so they don’t become limiting factors.

The key potential choke points to address with the trunk-based development pipeline are:

  • Merge requests rate to provide instant notification, alerts and automated delegation capabilities to avoid pending merge requests accumulating, which will translate into costly rework by developers afterwards.
  • Build time execution with fast compilation and package creation to quickly have artifacts ready for deployment. Unit tests are a particular area to optimize with tools that can choose which part of the code requires testing using intelligent impact analysis.
  • Automated test execution may become the biggest bottleneck of all once execution times change from seconds to hours. Use parallel testing and tooling to run critical tests first, and execution tests with less constraints afterwards.

This integrated tooling will set the right foundations, so that you can scale trunk-based development across your organization. As we have seen, experience matters and there are best-practices and recommendations that will accelerate the deployment of trunk-based development in a more structured way.

Overcoming the challenges of trunk-based development?Trunk-based development is a structuring practice that impacts the day-to-day workflow of all developers and, in the end, the quality and speed of software delivery. Common challenges have been identified and can be overcome with good preparation and progressive changes in your organization.

The following challenges will be faced when migrating to a trunk-based development pipeline:

  1. Adoption: changing our own habits takes time, and this is so for changing an organization. Start small, gradually, and focus on early adopters to show the value of habits like committing frequently and running tests locally.
  2. Communication: coordination among team members has to be stronger as people interact much more on the same codebase. Clear communication channels and systematic processes will structure interactions and help with growing maturity.
  3. Merge conflicts: developers face code conflicts early on with trunk-based development and must learn to promptly address them to avoid regressing to the old ways. Ensure code ownership, work allocation in different areas, and synchronous code reviews.
  4. Feature management: multiple changes in a shared codebase that is continuously moving forward require ways to deactivate problematic bits of code. The team must gradually become proficient with feature flag implementation and removal.
  5. Code quality: a codebase with frequent changes is harder to refactor, especially for structural changes. Organizations have to set clear code ownership, refactor in small increments, and rely on automated tests, intelligent refactoring, and code review.
  6. Technical debt: the increased flow of iteration with multiple feature flags and more challenging code quality will accumulate technical debt much faster. That’s where the team must incorporate addressing technical debt as part of its standard workload, leveraging the pipeline with controlled limiting factors to keep improving quality at speed.

These challenges are continuous with trunk-based development and must be overcomed during its implementation, by leading by example, sharing the greater vision and forecasted difficulties, and defining a plan to gradually implement changes in the organization. As we have seen, trunk-based development is more than a just coding process–it’s a way of thinking about the coding process.

ConclusionTrunk-based development can be a game-changer for organizations looking to deliver software with quality at speed. Similarly to the flow of lean factories, organizations able to deploy this practice can benefit from the fast flow of code changes, which can be quickly adapted and delivered to customers to support business goals.

Implementation challenges are fundamental points to consider before launching such an initiative. Development team maturity must be assessed first to determine if the practice would suit the current maturity and character of the organization. For instance, a fully decentralized development team with many juniors would not be a suitable context for implementing trunk-based development.

Trunk-based development represents the imperative to master the end-to-end software pipeline integration to support a fast flow of iterations. It is not only about mastering the code workflow between developers—it also requires organizational alignment with mature processes and tooling that can keep up with a growing codebase.

It represents the move from Software Engineering to Quality Engineering for better and faster software delivery with trunk-based development at its core.

Interested in trying it out? You can start implementing powerful trunk-based development today with Semaphore.

The post Trunk-Based Development: The Key to Better and Faster Software appeared first on Semaphore.

View Details

We’re thrilled to announce that starting today, new Semaphore users will have access to our new Startup and Scaleup plans.

In the past year, we’ve introduced several new features to Semaphore, which were previously only available on our Enterprise plan.

We recognize that smaller teams with high growth targets can benefit from these features as well. That’s why we’re introducing the Startup and Scaleup plans, which come with a per-seat fee, ensuring that we continue providing the additional value that our users expect.

You can get more details of the new plans on our updated pricing page and our new documentation pages.

Key benefits New machines – Our newest generation of cloud machines are available on our new plans, offering twice the RAM, and disk space, as well as powerful new CPUs that can speed up your builds by up to 50%! * Self-hosted agents – Enjoy all of the advantages of our Semaphore CI/CD app while you run your jobs in an environment fully controlled by your team. Our self-hosted agents are simple to configure and support easy autoscaling to adapt to your CI/CD needs. * Metrics and insights – Measure the performance, frequency, and reliability of your builds over time and gain access to future updates in this area. This helps you identify areas for improvement and optimize your CI/CD processes. * Powerful deployment controls – Level up your CD pipelines with parameterized promotions and upcoming deployment targets. This allows you to automate more of your release process, reducing errors and increasing efficiency. * Enterprise-level security – Comply with the strictest security standards and regulations and utilize features like RBAC, Okta, SAML/SCIM, audit logs, pre-flight checks, OpenID Connect integration, IP allow list and many more. * Add-ons* – Each organization is unique, and on our new plans, you can buy add-ons like a dedicated cache server, container registry, or additional support packages to optimize your CI experience even further.

Startup planStartup is now our default paid option and replaces the older Standard plan.

For a starting price of $10, you get the highest value package on the CI/CD market. We won’t limit you as you grow either. Our pricing is still usage-based, so whether you need more seats or more build minutes, just use what you need and pay at the end of the month.

On the Startup plan, you can run up to 80 jobs in parallel, and connect 40 self-hosted agents to your organization.

Scaleup planOur Scaleup plan is the upgraded version of our Enterprise plan. With this plan, you’ll receive a pack of free resources each month, and an equal amount of credits that you can use to pay for seats, machine time, storage, and egress.

The plan starts at $1,800, and any unspent credits from one month will be transferred to the next one, so you never lose any unused credits. Additionally, any additional spending on top of the credits will simply be added to your monthly bill. This gives you the flexibility to adjust your usage to match your business needs and ensures that you’re never paying for more than you need.

Frequently Asked Questions➟ How can I upgrade to a new plan?

If you’re an owner of Semaphore organization, you can easily upgrade your plan to Startup within the Plans & Billing page in-app. If you are interested in Scaleup, please contact our customer success team at customersuccess@semaphoreci.com

➟ Do I have to migrate?

You do not have to migrate to new plans. If you are on one of the older plans you don’t have to upgrade, however, if you create a new organization you will only be able to pick one of the newer plans that are available.

➟ Will these new features and machines be available on the legacy plans?

Organizations on older plans will keep access to all the features and environments that are currently available to them. Access to self-hosted agents, new machines, and new features will be available only on the new plans though.

➟ What are the key differences between Startup and Scaleup?

Parallelism and the number of self-hosted agents on Startup are restricted to 80 and 40, respectively. Organizations on the Scaleup plan gain access to unlimited parallelism and self-hosted agents, additional security features, flexible billing options, annual payment plans, a dedicated customer success manager, priority support, and many other benefits.

If you have any additional questions regarding the new Semaphore plans, feel free to reach out to us at support@semaphoreci.com, and we’ll be happy to help.

Happy building!

The post Introducing Startup and Scaleup: Our New Paid Plans appeared first on Semaphore.

View Details

The last CppNorth 2022 was announced with Chandler Carruth scheduled to give a keynote, where he showed the results of a new scientific experiment. The keynote was titled: Carbon Language: An experimental successor to C++. But why do we need a successor and where did this idea come from?

The last day for the CppCon 2019 conference came with a surprise talk by the same Chandler Carruth and Titus Winters, where they discussed What is C++? I recommend that you listen to this talk as it is very entertaining and informative. I think the most interesting phrase and idea transmitted during the first part of the talk is that “languages are tools and we have to use the right tool for the job.”

At the end of the talk, they made different proposals intended to address some of the common problems that C++ developers face, these proposals were:

  • Performance critical software. We are not only talking about speed but also about latency, memory utilization, and battery utilization among other things.
  • Software evolution, and language evolution. C++ was written decades ago, language designers also make mistakes and it should be possible to improve and upgrade according to the changes that most languages include these days–there are always, however, people in the community opposed to changing things.
  • Simple and easy to read, understand, and write. I think this quote from John Carmack sums it up well: “An enormous virtue of plain old C is that most senior developers are capable of writing their own C compiler, and knowing your tools at that level is valuable! I would need a lot of study time to write a C++ compiler.”
  • Practical safety and testing. Safer APIs, cheap security mitigations, and comprehensive testing methodology using Continuous Integrated (CI) unit and integration tests. All untrusted inputs are continuously fuzzed and all tests use checking build mode (sanitizers, asserts).
  • Fast and scalable development: modules, essentially.
  • Hardware, OSs, Environments. Define what we are committing to support and keep that rolling forward predictably. We’re not going to go out of the way to break your 20-year old platform but we may not go out of our way to keep supporting it either.

Could we improve C++ or use another language?Carruth gave us information in the last CppNorth 2022. He started by mentioning the proposals for the previous talk and discussing why they weren’t possible to realize for C++. The main problem is the accumulated decades of technical debt, during which the community was prioritizing backward compatibility, which prevents fixing technical debt.

Is there an existing language that could be used in place of C++? Languages using garbage collection are great but we are not going to consider them because they have a performance cost and most C++ developers consider that a trade-off they don’t want to make.

The Rust programming language is a good option because it’s memory safe and it could fit very well as a low-level language replacement, but keeping in mind the following relationships, it doesn’t fit well:

  • C –> C++
  • JavaScript –> TypeScript
  • Objective C –> Swift
  • Java –> Kotlin
  • C++ –> Rust?

Rust is a language that works perfectly well and it’s very recommendable if you are starting a project right now, but if you need to move a C++ ecosystem to Rust with very deep dependencies, it’s a very hard task. It’s not something that I recommend trying.

What we need is a successor language. A successor language is a language built within an existing ecosystem, without bootstrapping a new one, that provides interoperability, optimizes the learning curve and adoption path, and ideally has tool-assisted migration support. It’s essentially a superset for its ancestor language.

What is the Carbon programming language?Carruth started his talk with the question C++: What Comes Next? and guided listeners through some questions from the community like why we cannot improve C++, why we cannot use the Go language or another garbage-collected language, or even why we cannot use the Rust language (or another low-level language), before jumping to the need to create a real successor language for C++: Carbon.

Carbon took the proposals for improving C++ and incorporated them into its design goals, but they needed to add a new and critical goal, maybe the most important one: Interoperability with and migration from existing C++ code.

As defined on its website, we can establish the following definition and justification for the creation of the Carbon language:

“Carbon is fundamentally a successor language approach, rather than an attempt to incrementally evolve C++. It is designed around interoperability with C++ as well as large-scale adoption and migration for existing C++ codebases and developers. […] With this approach, we can build on top of C++’s existing ecosystem, and bring along existing investments, codebases, and developer populations.”

Will Go or Dart disappear?Because this initiative started at Google, where Carruth works as Technical Lead for Google’s Programming Languages and Software Foundations, one might worry that the support for Golang or even Dart could be decreased. Google is a big company, however, and they develop software for different platforms and very different projects. This means that they need different languages for different things.

Go is in use by Google in production for a couple of services (i.e. Google’s download server at dl.google.com) but, as Carruth said, migrating from big C++ projects is very hard and it won’t be done.

Dart is a language on top of JavaScript and other languages with the mission to be the main language for the Flutter project, addressing the creation of cross-platform apps. Because Flutter doesn’t support C++, Carbon is essentially useless in this environment and therefore the Dart language isn’t going to be affected.

How do we develop in this new language?During Core C++ 2022, Jon Ross-Perking (a Staff Software Engineer at Google who was working in the Carbon language for 2 years) talked about the syntax and trade-offs for the Carbon language.

The initial part of the talk emphasized the experimental nature of the project at the moment, but I would like to make a few observations about the ecosystem starting with the tooling. Ross-Perking revealed the following tooling goals for the project:

  • Compiler. The compilation of the code is very important, of course.
  • Formatter. Getting the code formatted correctly.
  • Automatic language upgrades. Tidy our code at a higher level than the formatter does.
  • IDE and LSP support.
  • Refactoring support.
  • Package manager.

About writing code, keeping the context where we are developing is important but, do we need to keep that amount of information? Carbon is simplifying the context by giving specific reserved words to make the tooling easier, and code more readable. These reserved words are:

  • var – used for declaring variables.
  • class – used for defining classes.
  • fn – used for defining a function.
  • interface – used for defining an interface.
  • let – used for declaring a constant.

As an example for each one is shown below:

var radius: Printer(Circle);class Circleclass Printer(template t:! Type)fn Draw()interface Shapelet Pi: f32 I think we can jump into the details of the syntax after this and check out an example of code. But before we start to browse the code, let me say that this language is still experimental and, as with most of the experiments, it could evolve or even disappear. At the moment, the project looks promising and that’s a good signal.

About syntax, this is still not the first release, which means that it could change from what you see here. You can check the design document where the designers and developers are contributing their proposals and changes, if you want to stay up to speed with the latest developments.

import Math;// Returns the smallest factor of `n` > 1, and// whether `n` itself is prime.fn SmallestFactor(n: i32) -> (i32, bool) { let limit: i32 = Math.Sqrt(n) as i32; var i: i32 = 2; while (i <= limit) { let remainder: i32 = n % i; if (remainder == 0) { Carbon.Print("{0} is a factor of {1}", i, n); return (i, false); } if (i == 2) { i = 3; } else { // Skip even numbers once we get past `2`. i += 2; } } return (n, true);} We are using two different functions here. Looks like the functions inside of the Carbon package are imported by default and we need to import the Math package so we can use it.

We also used two constants (let). The first one (limit) is a constant for the whole function, while the second one (reminder) is a constant in the scope of the while loop, and it’s created and bound to the value in every iteration. The limit constant also declares a type different from the return of the Math.sqrt function, but it’s cast (or converted) using the as i32 suffix.

In the return, we can see the definition of a tuple. We are returning two values instead of only one based on this technique. Languages like Python, Erlang, or Elixir use this type of data. This makes it possible to create a set of values to be handled in different situations.

Roadmap: looking into the future Carbon is an experiment at the moment, but you can check the roadmap and see how it is progressing. By comparing the previous entries for the same document we can see that the expected release (stopping the experiment) was slated for 2023, then 2024, and now it has been moved to 2025-2026. However, one thing we can see in the Github Insights is that Carbon is moving fast and it’s not stopping.

Is it going to be a real replacement for C++? We can only guess at the moment but keep in mind other initiatives like Swift or Kotlin. Carbon could be accepted and adopted by most of the big companies that wanted to solve the issues Carbon is addressing, issues addressed by the proposals we enumerated in the beginning of this article.

The post Will Carbon Replace C++? appeared first on Semaphore.

View Details

Actions and examples are more important than titles or positions when it comes to leadership. People don’t follow titles, they follow those who they admire and respect. Certain qualities, characteristics, and actions must be applied in order to lead a team or gain followers. While a leader in a software firm should be skilled technically, they should also possess extraordinary team management skills to help command respect from their teams and be tactically and strategically skilful to ensure that customers are happy. The team leader is an important cog in the project wheel. Therefore, I have identified 9 actions that every leader should perform to establish themselves as a respectable and successful leader.

1. Prioritize others and listen attentively to their concerns and ideasThis will earn trust and strengthen your relationship with your team members. A true leader doesn’t lead a team by dictating what should be done. They are always open to ideas from team members. This makes team members feel more invested in their project, because their ideas become a part of it.

In a software development team, a leader can prioritize others by:

  • Actively seeking input and feedback from team members on project plans and designs, making sure that everyone’s voices are heard and their ideas are considered.
  • Providing support and resources to team members to help them succeed in their tasks.
  • Recognizing and rewarding contributions to the team’s success. By trusting and respecting your team members, your team will trust and respect you. It’s a two-way street!

Success is best when it’s shared.

Howard Schultz

  1. Clearly communicate and outline your values, vision and objectivesTeam members will be willing to join a cause if they believe it is worthwhile. Effective communication is crucial for fostering collaboration, promoting clear understanding, and building trust within a team.

In my teams, at the project inception phase, we discuss and define the project vision, the set of values required and objectives that everyone can agree on. An example of this could be:

  • Setting clear and measurable goals for the team to work towards.
  • Identifying ways to increase the team’s productivity.
  • Discussing and then finalizing a set of metrics to measure the quality of the software.
  • Conveying specific deadlines for product releases so everyone understands their priorities.

To ensure that everyone is on the same page, the team leader should regularly measure progress towards these goals, gathering feedback and ideas from team members on how to improve. Additionally, the team leader should establish a set of values or principles that the team will adhere to in their work, such as a commitment to collaboration, continuous improvement, and customer satisfaction. By working together towards a shared vision and values, the team can improve their performance and achieve their objectives more effectively.

  1. Recognize excellenceGiving thanks and praising team members can help improve software quality in a few ways:

  2. Positive reinforcement: Recognizing and praising team members for their contributions provides positive reinforcement for good work and encourages team members to continue doing their best.

  3. Improved morale: When team members feel appreciated and valued, they have higher morale and motivation, which leads to better quality work.
  4. Increased collaboration: This fosters a positive and supportive work environment, which encourages team members to work together more effectively and share ideas, leading to better quality software.
  5. Better problem solving: When people feel good, they tend to be more creative and open to new ideas, which leads to more efficient problem solving and better software quality.

In my teams, we celebrate every possible moment from team release parties to an individual “Great Work” email from customers.

Celebrating small victories reminds the team of the overall goal.

  1. Communicate regularly and effectivelyClear and regular communication from a manager can help a software development team in several ways:

  2. Clarity of goals and expectations: By communicating clearly and regularly, a manager can ensure that the team understands the project goals, timelines, and expectations,

  3. Improved collaboration: Clear and regular communication helps team members to share ideas and information more easily, and to work together more effectively to achieve the project goals.
  4. Better problem solving: Clear and regular communication can help to identify and solve problems more quickly, and can also help to manage risks, and make sure that the team is following the best practices.
  5. Increased accountability: When team members know what is expected of them, and receive regular updates on progress and performance, they may be more accountable for their work and feel more motivated to perform at their best.
  6. Greater transparency: Clear and regular communication can help to build trust and transparency within the team and also with stakeholders, by keeping everyone informed about the project status and progress.

A good leader should also be able to create an environment where the team is open to any kind of constructive criticism or feedback. Constructive criticism/feedback helps to improve individual and team performance by providing specific, actionable insights and promoting open, ongoing communication.

Note: It’s important that the manager should be able to balance the level of communication, not overwhelming the team with too much information, but also not leaving them uninformed. They should also encourage the team to communicate among themselves, in order to build a strong team dynamic.

  1. Be decisive and show others how to actBe a person people want to follow, and act quickly and effectively when making crucial decisions. A decisive manager can help to make a software development team more productive in several ways. Let’s take a look at an example to help illustrate this.

Example: A customer reported that they are faced with a critical bug in the production system. The team quickly assesses the situation and determines the root cause of the issue. The manager reviews the root cause and the plan of action made by the development team to ensure that everything is in place. The team quickly works on the solution and implements the necessary fixes. Throughout the process, the manager is in touch with the team and receives regular updates for each completed milestone. This way they are able to answer any queries of the organization as well as customers during the entire process. By being decisive, available and leading by example, the manager is able to effectively manage the crisis and restore normal system operations.

Note: It is important to note that while a decisive manager is important, they should also be able to weigh the pros and cons of different options, and make decisions that are in the best interest of the team and the project. They should also be open to feedback and be able to change course if needed.

  1. Delegate to team members who are up for challengeIn a team, there is no one-man show, but there are always a few team members who are ready to take on new responsibilities along with their daily work. Identify those members and help them develop the skills that are necessary to take on new tasks. This will not only increase their productivity but will help them grow in their career path. Doing this will bring many benefits:

  2. Specialization: By delegating specific work to specific team members who have the appropriate skills and experience, the team can take advantage of each member’s strengths and expertise, which can lead to more efficient and effective work.

  3. Increased ownership and motivation: When team members are given more responsibility and autonomy, they may feel more invested in the work and be more motivated to perform at their best. This can lead to better quality work and improved productivity.
  4. Improved time management: Delegating work to team members can help to distribute the overall workload more evenly and can help to ensure that deadlines are met and that the team is working as efficiently as possible.
  5. Better problem solving: By delegating work to different team members, the manager can ensure that multiple perspectives are being considered when solving problems, which can lead to more effective problem-solving and better software quality.
  6. Increased collaboration: By delegating work to different team members, the manager can encourage collaboration, by promoting open communication and teamwork.

Note: It is important to note that delegating work effectively requires the manager to trust the team members to carry out the work competently and also to provide them with the necessary resources and support to be successful. The manager should also be able to give constructive feedback when/where it is needed, to ensure that the team members are developing their skills and knowledge.

  1. Trust and respect is a two-ay streetPeople who show respect to others are easier to treat with respect in turn. A software leader should respect their fellow team members for several reasons:

  2. Mutual trust and respect: A leader who respects their team members is more likely to earn their trust and respect in return. This can lead to a more positive and productive work environment, where team members are more willing to collaborate and work together.

  3. Improved communication: When team members feel respected, they may be more willing to share their ideas and communicate openly with their leader. This can help to identify and solve problems more quickly, and can also help to manage risks, and make sure that the team is following the best practices.
  4. Better problem solving: When a leader respects the team members, they are more likely to listen to their ideas and consider different perspectives when solving problems, which can lead to more effective problem-solving and better software quality.
  5. Increased motivation and productivity: When team members feel respected and valued, they may have higher morale and motivation, which can lead to better quality work and improved productivity.
  6. Greater accountability: When team members feel respected and valued, they may be more accountable for their work and may feel more motivated to perform at their best.
  7. Professionalism: Showing respect to team members is a fundamental aspect of professionalism, and it is an essential element for a leader to establish a positive and respectful work environment to foster a positive culture in the team.

Note: It is important to note that respect should be mutual, and that a leader should also expect respect from their team members in return. A leader should also be respectful of diversity and individuality, and should not tolerate any form of discrimination or harassment in the team.

  1. Motivate and inspireMotivating each team member to improve their skills and competencies can improve software project quality in several ways:

  2. Specialization: By focusing on each team member’s strengths, the team can take advantage of each member’s unique skills and expertise, which can lead to more efficient and effective work. This can also result in higher-quality software, as team members are able to utilize their strengths to their fullest potential.

  3. Increased ownership: When team members are given the opportunity to focus on and improve upon their skills, they may feel more invested in their work and be more motivated to perform at their best. This can lead to better quality work and improved productivity.
  4. Improved problem solving: By encouraging team members to improve their skills and competencies, the team leader can ensure that multiple perspectives and diverse skills are being considered when solving problems, which can lead to more effective problem-solving and better software quality.
  5. Professional development: Encouraging team members to improve their skills and competencies can help them to develop professionally, which can lead to higher job satisfaction and retention.

Note: It’s important to note that this process requires an effective feedback loop, wherein the team leader provides constructive feedback and coaching, and the team members take ownership of their own development. This will also require the team leader to have a deep understanding of the team members’ strengths and weaknesses, and the existence of resources and opportunities for the team members to improve their skills.

  1. Maintain momentumAll of life moves in cycles – up & down, boom & bust. There will be periods when morale is lower or goals are not being achieved as forecast. It is the responsibility of the leader to remain calm, provide support and ensure that the plan stays on course.

ConclusionThe best leadership advice is to observe the actions of other successful leaders. Aside from this, the aforementioned actions are crucial to developing a strong leadership presence and a devoted team or followership. Good leadership begins with concern for others and a desire to see success benefit everyone. Even if leadership isn’t the goal, these actions can still help build success and improve relationships.

The post 9 Tips for Becoming a Successful Tech Leader appeared first on Semaphore.

View Details

Software testing has undergone a significant transformation in recent years. It has evolved from being an isolated, time-consuming activity performed at the end of the development cycle, to an integrated and ongoing process tied to customer success. Various factors have driven this shift, including the rise of agile development methodologies and the need for faster and more efficient testing processes. In this article, we’ll explore, alongside Unity VP of Engineering Services Alan Page, the evolution of software testing and its impact on the software development industry, and discuss how testing practices have changed the way software is built and delivered.

Edited transcriptionAlan Page started his career in tech in the early nineties. Upon working on diverse jobs, he didn’t take long to specialize in testing, API testing, and test tool writing. Moreover, his long career also led him to pursue infrastructure, as well as software build processes and tools even before continuous integration (CI) practices became popular. Nonetheless, Alan’s experience in testing would lead him to work at Microsoft doing network testing and eventually teaching testers, designing courses, and guiding clients on how to use Microsoft’s products.

In 2008, as a way of writing down his knowledge from educating teams on testing subjects, Alan published How We Test Software at Microsoft. With a few chapters written by his colleagues Ken Johnston and BJ Rollison, Alan’s book became a milestone on the subject and crowned him as a testing authority. While Alan admits that part of the book has become dated, he is confident that another part of it stands up to this day as a valuable source of knowledge in testing.

Around five years ago, Alan joined Unity, a 3D engine popular for creating video games, to work doing both testing and infrastructure. After decades of working in the industry, Alan recognizes that, as time went by, he now cares “more and more about quality every day”, as his work now focuses on “improving quality and customer experience.” To this end, he now has “become less concerned about testing,” and believes that “testing is one way in which we achieve quality, but it’s not the only way.”

Dedicated testers or developers testing?Does defining testing as one of many ways of achieving quality challenge the role of testers and testing? While Alan points out that, in the future, dedicated testers will not be as needed, this doesn’t mean software will stop demand testing.

Functional correctness, no matter what you are working on, is, today, the job of the developer,” says Alan. As functionality depends on the developers, dealing with functional bugs, those related to product functionality, should be in their domain. Therefore, developers are responsible for “monitoring to understand how it’s being used [the code] and to find the potential issues that need to be fixed.”

Withal, Alan concedes that complex software will still require dedicated testers. However, their role will not be separated from the rest of the team and the product’s success. Dedicated testers should be committed “to find bugs that really could only be found by someone using the product end-to-end and thinking about customer outcomes”. Testing should not be involved externally (and even less bypassed) but integrated into production and shape how teams think and work.

From that perspective, Alan considers that “everything needs to be collaborative from day one”. Testers should pair with developers and help them understand testing techniques and ideas to the point “everybody should do some aspect of testing”, following a philosophy in which every team member“ should know a lot about a little and a little about a lot.” Hence, even though some individuals are going to be more proficient in certain tools than their colleagues, it is crucial not to rely on their knowledge to avoid growing dependent on them.

Testing and release cyclesThe transformation of testing has also had an impact on software delivery. Long shipping cycles have become a thing of the past in most industries, so customers can (and have become used to) get the latest software faster than ever before.

Not so long ago, Alan recalls, testing was a sort of releasing bottleneck that halted release on the premise of testing and finding bugs so they could release a rather perfect product; However, Alan believes that “these days, testing is truly about accelerating the business and making things move faster.”

What’s more, aside from delivery frequency, testing plays a central role in finding the right balance between delivering earlier and creating a superb product. CI testing toolings —such as code quality checkers and static analysis tools such as linters— have transformed the developer’s role to be more testing oriented and make them responsible for balancing quality and release time,

Delivering and updating on-premise softwareWhile testing and CI are meant to feed shorter feedback loops. This is also the case for on-premise software, whose feedback loops are slower than, for example, web services.

On-premise software can be tracked with DORA metrics, a popular set of metrics that serve to measure product success. “Just because it’s an on-prem product doesn’t mean you can’t use the traditional DevOps metrics,” Alan affirms. These metrics are:

  • Deployment frequency
  • Mean leader time for changes
  • Mean time to recover
  • Change failure rate

Another aspect to consider when developing a product to be delivered on-premise is the customer’s preference for update cycles. In his experience working at Unity, Alan says that many customers working with on-premise versions choose to stick with it for as long as their project goes to prevent having to deal with update changes. Despite this being mostly a risk assessment IT choice, customers should be willing to adopt the latest iteration of your product and not be forced to.

Of course, delaying updates is not the case for every user; many others are willing to update to get the latest features, solve bugs, and security updates. Hence, what’s important is making the latest builds available for those who want them and allowing those who prefer past versions to go with them. In this regard, Alan advises companies to implement a versioning model: “You can have one version scheming, Semantic Version or whatever, that ships yearly; another one that ships monthly; another one that ships weekly, and customers can choose which train to get on depending on, well, initially their aversion to risk.”

Fixing legacy code through testingAlan recognizes that dealing with legacy code can frustrate testers. The organization they work for might be unwilling to fix a bug to prevent breaking the code, even if affects the end customer experience. Besides, legacy code documentation might be incomplete properly, or the people who worked on it and knew about it might as well have left the organization long ago.

While fixing legacy code might be difficult —and even advised against—, Alan encourages developers to push over the line. Working with legacy code, he says is a matter of learning to deal with the unknown: “a lot of times the legacy code that no one wants to touch is also because no one understands it, and there may be parts of it that you understand why nobody wants to touch, but there may be parts of that code that may be okay to touch.

As a way of learning more about legacy code, Alan recommends writing unit tests. Unit tests are automated tests that test individual units or components of the software, such as functions, methods, or classes, in isolation from the rest of the system. The goal of unit testing is to validate that each unit of the software application is working as intended.

When reading and working with legacy code, unit tests can serve as a valuable source of information about the code’s design and functionality, allowing developers to understand how it works and make changes with more confidence. In this way, it is possible to determine the morphology of the bug and obtain “a better idea of where to look and what the risks may be in making a change.”

The bottom lineAlan has a podcast, ABTesting, on testing and other software development topics. Visit Alan’s blog, angryweasel.com, to learn more about his views and read his published works. You can also follow him on Twitter.

The post Alan Page on Testing: From Past to Future appeared first on Semaphore.

View Details

Go recently introduced an official vulnerability scanner that promises to best every other third-party tool out there: govulncheck. This is a tool that understands which modules and standard library functions your Go project is using and gives warnings about known vulnerabilities that can affect the application.

In this article, I want to explore how it works and how we can combine it with CI/CD to secure our Go projects.

The state of Go’s securityAs of September 2022, the state of security scanning in Go was disappointing. All we had were a few third-party, ruleset-based scanners like gosec or Snyk code checker. What was missing was a tool with access to a Go-specific known vulnerability database.

Enter govulncheckerThe Go security team introduced govulncheck in September 2022. Govulncheck is an open-source command line utility that can analyze code and give warnings about known issues in Go modules or its standard library. Behind the scenes, govulncheck grabs its data from the Go vulnerability database, which is maintained and curated by the Go security team.

The Go Security Team curates vulnerability reports.Compared to other security tools, govulncheck has a few important advantages:

  • Smart: the tool warns you only if you actually use vulnerable code. This means it’s a lot less noisy than the likes of npm audit, which only scans the package manifest.
  • Comprehensive: the database feeds from multiple sources, including internal reports, package maintainer submissions, the National Vulnerability Database (NVD), and the GitHub Advisory Database.
  • Official: the Go developer team maintains the tool. And it will eventually find its way into the Go distribution itself.
  • Curated: the database is curated by the Go security team, implying a higher level of supervision.

Getting started with govulncheckGetting started with govulncheck is as straightforward as installing the CLI with go install and running it in the project folder:

$ go install golang.org/x/vuln/cmd/govulncheck@latest$ govulncheck ./... Check your PATH if you get a “command not found” error. Go installs binaries in $GOPATH/bin. So, you may need to update your environment:

$ export PATH=$PATH:$HOME/go/bin# or$ export PATH=$PATH:$GOPATH/bin Govulncheck is still experimental, so it has only a few options. We can supply -test to scan test files and -json for JSON output, and that’s it.

To see govulncheck in action, let’s fork and clone the semaphoreci-demo/semaphore-demo-go repository. As you can see, I’m using Go version 1.19 here:

$ go versiongo version go1.19 linux/amd64$ govulncheck ./...govulncheck is an experimental tool. Share feedback at https://go.dev/s/govulncheck-feedback.Scanning for dependencies with known vulnerabilities...Found 3 known vulnerabilities.Vulnerability #1: GO-2022-1144 An attacker can cause excessive memory growth in a Go server accepting HTTP/2 requests. HTTP/2 server connections contain a cache of HTTP header keys sent by the client. While the total number of entries in this cache is capped, an attacker sending very large keys can cause the server to allocate approximately 64 MiB per open connection. Call stacks in your code: main.go:76:28: github.com/semaphoreci-demos/semaphore-demo-go.main calls net/http.ListenAndServe Found in: net/http@go1.19 Fixed in: net/http@go1.19.4 More info: https://pkg.go.dev/vuln/GO-2022-1144Vulnerability #2: GO-2022-1039 Programs which compile regular expressions from untrusted sources may be vulnerable to memory exhaustion or denial of service. The parsed regexp representation is linear in the size of the input, but in some cases the constant factor can be as high as 40,000, making relatively small regexps consume much larger amounts of memory. After fix, each regexp being parsed is limited to a 256 MB memory footprint. Regular expressions whose representation would use more space than that are rejected. Normal use of regular expressions is unaffected. Call stacks in your code: github.com/semaphoreci-demos/semaphore-demo-go.init calls github.com/lib/pq.init, which eventually calls regexp/syntax.Parse Found in: regexp/syntax@go1.19 Fixed in: regexp/syntax@go1.19.2 More info: https://pkg.go.dev/vuln/GO-2022-1039Vulnerability #3: GO-2022-0969 HTTP/2 server connections can hang forever waiting for a clean shutdown that was preempted by a fatal error. This condition can be exploited by a malicious client to cause a denial of service. Call stacks in your code: main.go:76:28: github.com/semaphoreci-demos/semaphore-demo-go.main calls net/http.ListenAndServe Found in: net/http@go1.19 Fixed in: net/http@go1.19.1 More info: https://pkg.go.dev/vuln/GO-2022-0969=== Informational ===The vulnerabilities below are in packages that you import, but your codedoesn't appear to call any vulnerable functions. You may not need to take anyaction. See https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheckfor details.Vulnerability #1: GO-2022-1143 On Windows, restricted files can be accessed via os.DirFS and http.Dir. The os.DirFS function and http.Dir type provide access to a tree of files rooted at a given directory. These functions permit access to Windows device files under that root. For example, os.DirFS("C:/tmp").Open("COM1") opens the COM1 device. Both os.DirFS and http.Dir only provide read-only filesystem access. In addition, on Windows, an os.DirFS for the directory (the root of the current drive) can permit a maliciously crafted path to escape from the drive and access any path on the system. With fix applied, the behavior of os.DirFS("") has changed. Previously, an empty root was treated equivalently to "/", so os.DirFS("").Open("tmp") would open the path "/tmp". This now returns an error. Found in: net/http@go1.19 Fixed in: net/http@go1.19.4 More info: https://pkg.go.dev/vuln/GO-2022-1143Vulnerability #2: GO-2022-1095 Due to unsanitized NUL values, attackers may be able to maliciously set environment variables on Windows. In syscall.StartProcess and os/exec.Cmd, invalid environment variable values containing NUL values are not properly checked for. A malicious environment variable value can exploit this behavior to set a value for a different environment variable. For example, the environment variable string "A=B\x00C=D" sets the variables "A=B" and "C=D". Found in: os/exec@go1.19 Fixed in: os/exec@go1.19.3 More info: https://pkg.go.dev/vuln/GO-2022-1095Vulnerability #3: GO-2022-0988 JoinPath and URL.JoinPath do not remove ../ path elements appended to a relative path. For example, JoinPath("https://go.dev", "../go") returns the URL "https://go.dev/../go", despite the JoinPath documentation stating that ../ path elements are removed from the result. Found in: net/url@go1.19 Fixed in: net/url@go1.19.1 More info: https://pkg.go.dev/vuln/GO-2022-0988 Govulncheck reveals that I have three security issues in the project; all of them can be resolved by switching to a newer Go version, as the affected packages belong to the Go standard library. The tool can also warn us about other problems in imported modules. These are “informational” messages because we’re not using the affected functions, so we should be safe.

Govulncheck quirksGovulncheck is an experimental tool, so finding a few quirks shouldn’t be too surprising. During the creation of this tutorial we found two issues.

The first one appeared in projects using C extensions. Importing C code can make govulncheck fail:

$ govulncheck ./...Scanning for dependencies with known vulnerabilities...govulncheck: Packages contain errors:/usr/local/go/src/runtime/cgo/cgo.go:33:8: could not import C (no metadata for C)/usr/local/go/src/os/user/cgo_listgroups_unix.go:19:8: could not import C (no metadata for C)/usr/local/go/src/net/cgo_linux.go:12:8: could not import C (no metadata for C) The workaround we found is to disable the cgo package with: export CGO_ENABLED=0.

The second problem we encountered was high memory usage. According to this issue: govulncheck ate all available memory and got killed; govulncheck builds a call graph in memory that uses up a lot of it in big projects. So, before adding govulncheck to your project, do some trial runs to see how much memory it needs. If you experience this problem, you can use a system with more memory (e.g. choosing a bigger CI machine) or run govulncheck on smaller subsets of the project’s code.

Using govulncheck in the CI pipelineThe main benefit of having a CLI tool like govulncheck is that we can integrate it with our continuous integration pipeline. Implementing the integration lets everyone on the team know when a security issue emerges in the project, reducing the chance of releasing vulnerable software.

Adding govulncheck to the CI pipeline is very straightforward. The demo repository I cloned earlier already has a started pipeline, so I only need to add one job at the end.

We can add govulncheck into our CI pipeline by creating a new job. In this section, we’ll use the example pipeline in the semaphoreci-demo/semaphore-demo-go we forked earlier. The project already has a starter pipeline that looks like this:

An example CI pipeline for a Go project. The pipeline features static, unit and integration tests.The job to create has the commands shown below. Adjust the sem-version command as needed. We’ll choose Go 1.19 to verify that the job fails.

checkoutsem-version go 1.19go install golang.org/x/vuln/cmd/govulncheck@latestgovulncheck ./... After the changes, the pipeline will look like this:

Adding a govulncheck test to a CI pipelineRunning the pipeline should throw an error since we’ve not yet fixed the security issues.

The job fails when govulncheck detects vulnerabilities in the project. This prevents the pipeline from moving forward, preventing the release of unsafe software.Checking the job log reveals the same error that we encountered the first time.

Clicking on the job reveals the problems that were found by govulncheck.In this example, we can fix the problem by changing sem-version go 1.19 to sem-version go 1.19.4 in all the jobs in the CI pipeline.

ConclusionWe dare not live without automated security scanning, especially in a system-level language like Go. Govulncheck may not have the catchiest of names and, being in the experimental stage, it has its share of troubles, but there is no doubt that the Go security team made a giant leap forward with its release.

If this tool looks useful to you, check out the official VS Code extension, which lets you run security checks right in the IDE.

Thanks for reading!

The post Vulnerability Scanning in Go With Govulncheck appeared first on Semaphore.

View Details

React Native popularity and adoption is ticking up every day, and it is used to build many enterprise applications. As it is gaining more traction, more vulnerabilities are being discovered and reported. This means that it is crucial to keep the security of React Native apps in mind while building them. Because of its open-source background, RN apps are more prone to security vulnerabilities. This means that more vigilance is needed to keep them secure.

Since most of the logic for React Native apps is written in JS, the coverage or threat area is much larger than that of native apps. So, in essence, we have to ensure the security of both the native code and the JS code. Just like any other frontend app, the code of the app is shipped to devices for execution, which makes it inherently vulnerable to getting tampered with.

This article will look at some of the most common security threats for React Native apps and some of the tools and techniques that can be used to secure against them.

Tools & TricksLet’s look at some super valuable tools and techniques to make your React Native apps more secure. We will cover the areas of network security, data security, static analysis, dependency security, platform security, and more.

Secure Network Communication – SSL PinningAs much as 35% of attacks in 2019 were related to attempts at exploitation using Man in the Middle attacks. For mobile apps communicating with servers, it is necessary to ensure that communication is secure and not tampered with. SSL pinning is a technique that allows you to verify the identity of the server you are communicating with, helping you avoid MITM attacks.

Apps using HTTPS depend on CA and DNS to verify the server’s identity. A malicious actor can bypass this by setting up an unsafe certificate on the client’s devices and making all communication between the client and the server insecure.

SSL pinning allows you to verify the server’s identity by verifying the server’s certificate; this is done by verifying the server’s certificate fingerprint. SSL pinning will ensure that the communication is secure and has not been tampered with.

Here are a couple of tools for SSL pinning:

  • React Native SSL Pinning: This enables SSL pinning & public key pinning using OkHttp3 on Android & AFNetworking on iOS. To leverage the library, we override React Native’s fetch with import {fetch} from 'react-native-ssl-pinning'. It also supports multipart forms. Important note: you have to implement error handling for certificate expiry and might require an app update to update certificates.
  • React Native Pinch: an HTTP client that supports SSL pinning for React Native. It also wraps the fetch method.

Securing Stored DataToday’s apps rely not only on network data, they also store data locally on the device. This data can be sensitive and should be protected. Another critical aspect of security is to ensure that all data stored on the device is secure; this includes data stored in the app’s local storage, database, cache, etc. It is crucial to ensure that the data stored on the device is not accessible to other apps or users.

Fortunately, iOS and Android provide tools to secure data stored on devices using Keychain and Keystore, respectively. Here are some of the RN wrappers for these tools:

  • React Native MMKV is the fastest Key-Value storage for React Native apps by far; it relies on the native security of app files and also provides encryption. It is a wrapper around MMKV.
  • React Native Encrypted Storage is a module that helps secure data by providing a wrapper around Android’s EncryptedSharedPreferences and iOS’ Keychain. It comes complete with support for TypeScript.
  • RNSecureStorage is another option that uses Keychain for secure storage with iOS. While for Android, it uses secure-preferences for below API 23 and Android Keystore for above API 23. It also allows you to make data accessible only after specific user actions on the device, like, when the passcode is set, when a device is unlocked, etc.

However, it is essential to note that these tools are not a silver bullet. They are not a replacement for proper data encryption. They are just a way to secure data stored on the device. For an added security layer; It is better to encrypt data before storing it on the device. Or better yet, don’t store data on the device in the first place.

Obfuscation of JS CodeObfuscation is a technique that makes the code harder to read and understand. Obfuscation makes it harder for attackers to understand code and exploit it. For React Native apps, obfuscation of JS code is more important than obfuscation of native code, as most of the app logic resides in JS. General obfuscation of JS code done by default modifiers or Hermes conversion of JS to bytecode is not enough. It is essential to use a tool that can obfuscate the code in a way that is harder to understand and exploit.

Here are some of the tools for obfuscation:

  • Jscrambler protects your JavaScript with polymorphic obfuscation, code locks, and self-defensive capabilities to prevent code theft, abuse, and tampering.
  • JSDefender provides JS obfuscation with control-flow flattening, tamper detection, and other in-app protection transforms.

Protection from SQL InjectionSQL injection is a code injection technique that can destroy your database. It is also one of the most common web hacking techniques. Be it the apps using the local database or API calls to remote servers, SQL injection is a threat to both. It is essential to protect your app from SQL injection attacks. All user input should be appropriately validated and sanitized before being used outside any UI scope. Sanitization will ensure that the user input is not malicious for the app.

Here are some tools for protection from SQL injection attacks:

  • Formik is a popular form library for React Native apps. It provides a way to validate and sanitize user input. It also provides a way to validate user input against a schema. It helps validate and sanitize user input before using it in any SQL query.
  • Validator.js is the library to validate and sanitize strings in your app; all user inputs should pass through this library before being used anywhere in the app.

Here is a more detailed guide on input validation to protect your app from SQL injection.

Segregation of Authenticated & Public PartsIn most applications, some parts are accessible to everyone, and components are accessible only to authenticated users or users with a specific role. It is essential to keep these parts of the application separate. Segregation will help you to keep your application secure. For React Native apps, this can mostly be done at the Javascript level with multi routers and multi bundles.

  • React Navigation allows you to create multiple routers in your application, So the authenticated and public parts of your application can be separated. Multiple routers will enable you to load authenticated parts of your Javascript only when the user is authenticated.
  • Re.pack can be a helpful tool for creating multiple bundles in your application. It enables you to deliver authenticated features of your application remotely.

Static Application Security Testing (SAST)Static Application Security Testing (SAST) is a type of application security testing that analyzes application source code, byte code, and binaries for software vulnerabilities. SAST tools analyze an application without actually running it, and can find vulnerabilities that are difficult to see with dynamic testing. SAST tools are also high-speed and can scan code in a CI/CD pipeline.

Here are some of popular tools for SAST:

  • SonarQube
  • Fortify
  • Checkmarx
  • Mend SAST
  • Synk

Checking Dependencies for VulnerabilitiesUsually, React Native apps use a lot of third-party libraries. It is essential to regularly check these libraries for vulnerabilities. Relying on a library that has a vulnerability can be a security risk. To keep our apps secure, we should regularly check our dependencies for vulnerabilities. It is important to make a dependency check part of the CI/CD pipeline for every PR or commit, so that you can be sure that the app is not using any vulnerable libraries.

Here are some tools that can help you check your dependencies for vulnerabilities:

  • OWASP Dependency Check
  • NPM Audit
  • Retire.js
  • Snyk

Jailbreak/Rooted Device DetectionJailbroken or Rooted devices, as well as emulators, sometimes bypass security measures provided by the OS. Attackers can use such devices to access sensitive data and perform malicious actions. It is essential to detect jailbroken and rooted devices and prevent them from accessing your app.

Here is a good tool to detect jailbroken and rooted devices:

  • Jailmonkey JailMonkey allows you to identify if a phone has been jailbroken or rooted for iOS/Android. It also lets you identify if the app is running in an emulator.

Checking for App TamperingApp tampering can be done by modifying the app binary or by modifying the app data. It is crucial to detect app tampering and prevent the app from running if it has been tampered with. This is especially important for apps handling sensitive user data or financial transactions, as it must be ensured that all transactions are triggered by a genuine app running on a genuine device.

Here are some of the tools that can be used to detect app tampering:

  • Google Play Integrity provides an attestation API to check whether your server is interacting with an unmodified binary, for paid apps it ensures that the current user account is licensed to use the application, and if the app is running on a genuine android device. Unfortunately, a React Native wrapper for Play integrity isn’t yet available.
  • React Native SafetyNet is a wrapper over Google’s SafetyNet API. The SafetyNet Attestation API is an anti-abuse API that allows app developers to assess an Android device that their app is running on. The API should be used as a part of your abuse detection system to help determine whether your servers are interacting with the genuine app running on a genuine Android device. Important note: Google has stated that they will discontinue SafetyNet in favor of Play Integrity.

Getting Deep-Linking RightDeep-linking is a way to open a specific screen in your app from a URL. It is essential to get deep-linking right in order to prevent phishing attacks. It is crucial to validate the URL before opening the screen. It is also necessary to make sure that private URLs are not accessible to everyone and, above all, that URLs are pruned before the app processes them.

For apps using third-party libraries with deep linking, it is vital to ensure that the libraries are not vulnerable to phishing attacks.

  • If you’re using React Navigation for deep-linking, please make sure to validate the URL before opening the screen.
  • If you’re using React Native Navigation, leverage React Native Linking API to create a custom deep-linking handler. Warning: handle it with caution.

Platform Security (Android/iOS)React Native apps work on both Android and iOS, so it is vital to keep the security of both platforms in mind while building your app. Any tools or techniques available to secure native apps are equally relevant to React Native apps.

Here are some Guardsquare tools to secure your app on both platforms:

  • DexGuard (Android) offers several security features for Android apps. It protects your app from reverse engineering, tampering, and other attacks. It can also obfuscate your code and save your app from decompilation.
  • iXGuard (iOS) iXGuard protects iOS applications. It secures iOS apps and SDKs using multiple layers of code hardening and runtime application self-protection (RASP).

App Distribution & SigningIt’s also essential to ensure that the application is signed correctly and distributed through official channels to prevent malicious actors from distributing tampered versions of the application. For large-scale apps developed by multiple teams, it is important to have a proper process in place to ensure that the application is properly signed and distributed. CI/CD pipelines should be used to automate signing and distribution of the application.

  • Fastlane match is a tool that can be used to manage certificates and provisioning profiles. It can create a private repository for certificates and provisioning profiles, and ensure that certificates and provisioning profiles are not leaked to the public.

Bring Security to CI/CDIn addition to these security considerations, it’s important to keep in mind the best practices for securing a CI/CD pipeline, such as implementing secure authentication and access controls and regularly monitoring for and addressing any security threats or vulnerabilities. Here are some security actions that should be implemented in your CI/CD pipeline:

For every PR before merging:* SAST checks * Checking for vulnerable dependencies * Ensure that storage is not used without encryption * Running automated tests to verify that authenticated parts of the app are accessible only with authentication

For release builds:* Restrict access to prod SSL certs by injecting them into the app at build time * Checking for deep-linking vulnerabilities before releasing the app * Signing the app with release certificates from the pipeline to prevent them from being leaked * Testing the hardening of release builds on jailbroken and rooted devices before releasing the app

ConclusionReact Native is an excellent framework for building cross-platform apps, but it is essential to keep security in mind. Whether you’re working with financial or personal data, it is crucial to keep your app secure. Security is an ongoing marathon, and we must keep ourselves up-to-date with the latest security threats and vulnerabilities.

The post Ready to Release? Securing Your React Native Apps appeared first on Semaphore.

View Details

Relational databases have a long, long history. The first appeared in the 1970s, and while the technology has certainly evolved, the relational model has proved the most popular over the last 5 decades.

Is there room for innovation after 50 years of history? The folks at Neon are showing us that one can, in fact, teach an old dog new tricks.

What is Neon?Neon is an open-source (Apache 2.0) alternative to AWS Aurora or Google’s Cloud SQL for Postgres. Neon is a serverless, scalable implementation of PostgreSQL that you can run on-premise or try through its managed service.

Neon decomposes the PostgreSQL architecture into two layers: compute and storage. The compute layer consists of stateless PostgreSQL running on Kubernetes, allowing pods to be scaled on demand — even to zero.

Persistence is achieved with the Neon storage engine, a custom-made layer that handles both transactions and data. The transaction log is processed through a set of redundant safekeeper services, while data pages are managed on disk by the pageserver.

Neon architecture splits the database into scalable compute and storage planes. Compute nodes can be started and stopped depending on demand.​

Currently, the managed service is running a free tech preview with some limitations that we’ll discuss later.

Neon’s killer feature: branchesNeon was launched in June 2021. Being a new project, the managed service may have fewer features than the competition. But Neon has one feature that, to my knowledge, no one else has: branches.

Every developer is familiar with branches. In Neon, branches work pretty much the same as in Git, except they cannot be merged (although there are plans to add schema-based merging in the future). You can, at any point, branch off the main trunk, effectively creating an “alternate timeline”.

Since branches in Neon are writable, this feature allows us to do things no other database engine can do. For instance:

  • Freely experiment without impacting the main branch.
  • Instantly back up the database. So, if data is lost by mistake, we can switch to the last good branch.
  • Simplify integration testing. Developers can run tests in disposable test-specific branches.
  • Safely try out automated database migrations on production.
  • Run analytics or machine learning workloads in isolation.

Instantly duplicate all the databases that serve to a specific cluster of microservices.

You can’t do any of these things on traditional database engines. Not easily at least. Some database engines like SQL Server have snapshots, which indeed can create instant copies of a database. But snapshots are read-only and this limits their utility. On most database engines, we have to resort to clunkier mechanisms like backup and restore or replication.

A few use cases for Neon branches.​

Branches are per-project. And a project can have multiple databases. That means that creating a branch duplicates all the databases in that project. We can take advantage of this project > database hierarchy to clone a group of related databases in one operation.

Getting started with NeonLet’s try out Neon’s managed service. To create a tech preview free account, just follow these steps:

  1. Sign up at neon.tech/sign_in.
  2. Click on Create a project.
  3. Click on Download env.txt. This file contains everything you need to connect to the database instance.
  4. Click on Settings and copy the project id.

We also need to generate an API Key, as shown below:

  1. Click on your avatar and select Account > Developer Settings > Create new API key
  2. Edit env.txt and add the following lines:
    • export NEON_API_KEY=Your-API-Key
    • export PROJECT_ID=Your-Project-ID
  3. Add the keyword export before every variable.

The final env.txt file should look like this example:

```

Connection detailsexport PGHOST=ep-random-name.us-east-2.aws.neon.techexport PGDATABASE=neondbexport PGUSER=Tommyexport PGPASSWORD=sekret1# Connection stringexport DATABASE_URL=postgres://Tommy:sekret1@ep-random-name.us-east-2.aws.neon.tech/neondb# Neon configexport NEON_API_KEY=MyApiKeyexport NEON_PROJECT_ID=random-name-140532

``` We’ll need this file to connect to the Neon database and API.

The Neon UIThe Neon dashboard includes an SQL editor to run commands and controls for creating branches or endpoints.

The managed service dashboard.​

On the Branches page, we’ll find options for creating a new branch. Here, you can select the what and when. You must choose the parent branch and how much data to include:

  • Head: the new branch is a copy of the current database.
  • Time: the branch has the parent’s data up to a specified date and time.
  • LSN: the branch has the parent’s data up to a specified log sequence number.

The create branch UI.​

Endpoints for the branches can be created on the same screen or on the Endpoints page.

Connecting to NeonNeon is a PostgreSQL database, so we’ll need to install the client tools. Check which version yours is running with:

$ psql --versionpsql (PostgreSQL) 15.1 (Ubuntu 15.1-1.pgdg20.04+1) Neon works best with versions 14 and 15 of the client tools. So, if needed, head to postgresql.org/download to get the latest release.

Now, let’s source the env.txt and try connecting:

$ source env.txt$ psqlpsql (15.1 (Ubuntu 15.1-1.pgdg20.04+1), server 14.6)neondb=> SELECT version(); version--------------------------------------------------------------------------------------------------- PostgreSQL 14.6 on x86_64-pc-linux-gnu, compiled by gcc (Debian 10.2.1-6) 10.2.1 20210110, 64-bit(1 row) You can also do browser-based authentication by running the command shown below. This will open a browser window and let you select the project you want to connect to.

$ psql -h pg.neon.tech We may also test the Neon API key with curl:

$ curl -s 'https://console.neon.tech/api/v2/projects' \ -H 'accept: application/json' \ -H "Authorization: Bearer $NEON_API_KEY"{ "projects": [ { "id": "calm-guy-140532", "platform_id": "aws", "region_id": "aws-us-east-2", "name": "test-project", "provisioner": "k8s-pod", "pg_version": 14, "locked": false, "created_at": "2022-12-22T18:16:13Z", "updated_at": "2022-12-22T18:16:13Z", "proxy_host": "us-east-2.aws.neon.tech" } ]} Automated branching with CI/CDOne of the most delicate parts of deployment is the database migration step because there is always the possibility of data loss. There are several techniques to make this process safer. But with Neon, we can leverage branches to make the process infallible.

Let’s say we have the following continuous deployment pipeline. Here, we run the database migration job on production before deployment. The problem with this setup is that we could be stuck with a corrupt database if the job fails.

Example initial pipeline. We have not added branch management yet.​

How can we make the process safer? Well, we can run the database migration script on a disposable branch. This will give us two critical pieces of information: that the migration works and how long it takes.

Once we’re done setting up the migration test job, the pipeline should look like the following picture. You should be able to adapt the following instructions to any CI/CD project.

The pipeline once we add a database migration test job on a Neon branch.​

Creating secretsBefore creating the job, we should upload env.txt to Semaphore as secret, so the Semaphore CI machine can connect with the database.

You can find your secrets by clicking on your organization icon on the top right of the Semaphore dashboard and selecting Settings. Click on Secret > New Secret.

Upload env.txt and name the secret “neon”:

The environment file has all the parameters needed to interact with the database.​

Writing helper scriptsNext, we’ll need to create a few helper scripts to manage branches through the API. Neon docs describe how it works.

The first script shows all existing branches in our project. Before trying it on your machine, ensure you have sourced env.txt. You’ll also need to have Bash and jq installed.

```

!/usr/bin/env bash# List the IDs of all existing branches IDsset -ecurl -s "https://console.neon.tech/api/v2/projects/$NEON_PROJECT_ID/branches" \ -H 'accept: application/json' \ -H "Authorization: Bearer $NEON_API_KEY" | jq -r ".branches[] | .id"

``` Execute the script to check that everything works (the main branch is never listed):

$ ./get-branches.shbr-blue-feather-415684 The second script creates branches. We’ll call it create-branch.sh:

```

!/usr/bin/env bash# Create a new Neon branch. Writes updated environment file.## Usage: create-branch.sh PARENT_BRANCH_NAME OUTPUT_FILE# Example: create-branch.sh main env-branchset -eset -uparent_name=$1outfile=$2parent_id=$(curl -s "https://console.neon.tech/api/v2/projects/$NEON_PROJECT_ID/branches" \ -H 'accept: application/json' \ -H "Authorization: Bearer $NEON_API_KEY" | jq -r ".branches[] | select(.name==\"$parent_name\") | .id")if [ -z "$parent_id" ]; then echo "Unable to find id of parent branch '$parent_name'" >&2 exit 1fibranch_data=$(curl -s -X POST "https://console.neon.tech/api/v2/projects/$NEON_PROJECT_ID/branches" \ -H "Accept: application/json" \ -H "Authorization: Bearer $NEON_API_KEY" \ -H "Content-Type: application/json" \ -d "{ \"endpoints\": [ { \"type\": \"read_write\" } ], \"branch\": { \"parent_id\": \"$parent_id\" }}")branch_endpoint=$(echo "$branch_data" | jq ' .endpoints[] | .host' | sed 's/"//g')branch_id=$(echo "$branch_data" | jq ' .branch | .id' | sed 's/"//g')if [ -z "$branch_endpoint" ] || [ -z "$branch_id" ]; then echo "Unable to create new branch" >&2 exit 1fiurl=$(echo "$DATABASE_URL" | sed "s/$PGHOST/$branch_endpoint/")echo "Created branch: $branch_id"echo "# Generated with create-branch.sh" > $outfileecho "export BRANCH_ID=$branch_id" >> $outfileecho "export PGHOST=$branch_endpoint" >> $outfileecho "export DATABASE_URL=$url" >> $outfileecho "Wrote environment file: $outfile"

`` We can now create a branch withcreate-branch.sh . The first argument is the name of the parent branch (typically,main`). The second argument is an environment file to write with the connection parameters for the next branch.

For instance, we can create a new branch and connect to it with the following:

$ ./create-branch.sh main env-branch$ source env-branch$ psql Finally, we need to be able to delete the branch we just created. For that, we need to provide the branch_id, which is randomly defined on branch creation (and stored in the environment file generated with create-branch.sh).

```

!/usr/bin/env bash# Delete Neon branch## Usage: delete-branch.sh BRANCH_ID# Example: delete-branch.sh br-dawn-shape-137746set -e set -ubranch_id=$1id=$(curl -s -X 'DELETE' \ "https://console.neon.tech/api/v2/projects/$NEON_PROJECT_ID/branches/$branch_id" \ -H "accept: application/json" \ -H "Authorization: Bearer $NEON_API_KEY" | jq ' .branch | .id ' | sed 's/"//g')echo "Deleted branch: $id"

`` When finished, commit all the new scripts into your repository. We’ll keep them in a folder calleddb-scripts` at the project’s root.

Add job to the pipelineNext, we’ll add a test job in the continuous deployment pipeline. We’ll open our workflow in the Editor and click on Add Block in the continuous deployment pipeline.

screenshot

We’ll use the prologue to update the Postgres client library:

sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -sudo apt-get updatesudo apt-get -y install postgresql-client-15sem-version ruby 3.0checkoutcache restorebundler installcache store Now we can run the scripts that we created earlier:

  1. Create a branch and switch to it.
  2. Run the migration script. In my case, I’m upgrading a Rails application, so I run bin/rails db:migrate. Change this command to suit your project. For example, if you’re working with Django use python manage makemigrations. The same thing on Laravel is achieved with php artisan migrate.
  3. Optionally run any tests.
  4. Delete the branch.

The job should look like this:

source $HOME/env.txt./db-scripts/create-branch.sh main env-branch./db-scripts/get-branches.shsource env-branchbin/rails db:migrate./db-scripts/delete-branch.sh $BRANCH_ID To finish the setup, enable the neon secret so the job can authenticate with the database.

Database migration test job added to the pipeline.​

Press the Run the workflow button to save your changes and test the workflow.

The final pipeline.​

Technical preview limitationsAt the time of writing Neon is in a technical preview stage. While the managed service is entirely free, it comes with some limitations:

  • You can only have one project per user. But a project can have multiple databases.
  • A project can have up to nine branches in addition to the main branch.
  • You can have up to three endpoints. One is always reserved for the main database. That leaves only two endpoints accessible for two other branches.
  • The size limit is 3GB per branch on the free tier.
  • Point-in-time branches can only go up to seven days into the past.
  • There are no backup or restore options on the UI. The only alternative seems to run pg_dump neondb and take a remote backup.
  • There are a few observations around importing data from another PostgreSQL instance or a backup.
  • There is a 100 concurrent connections limit. You can enable connection pooling to raise the limit to 1,000 connections.

The good news is that if you like the database, you can always run it on-premise or in your cloud of choice to remove these limitations.

ConclusionNeon’s branching feature presents new options for development and database management. Even in its current technical preview stage, I can see great potential for this engine. Of course, we’ll have to see how the project evolves, especially how the final pricing model will turn out.

I hope you found this project interesting, and if so, you might want to consider contributing to Neon.

Thanks for reading!

The post A First Look at Neon: A Postgres Database That Branches appeared first on Semaphore.

View Details

Ruby has joined the ranks of languages capable of targeting WebAssembly with its latest 3.2 release. This seemingly minor update might be the biggest thing that has happened to the language since Rails, as it lets Ruby developers go beyond the backend. By porting their code to WebAssembly, they can run it anywhere: on the frontend, on embedded devices, as serverless functions, in place of containers, or on the edge. WebAssembly has the potential to make Ruby a universal language.

What is WebAssembly?WebAssembly (commonly shortened as Wasm) is a binary low-level instruction format that runs on a virtual machine. The language was designed as an alternative to JavaScript. Its aim is to run applications on any browser at near-native speeds. Wasm can be targeted from any high-level language like C, Go, Rust, and now also Ruby.

Wasm became a W3C standard in 2019, opening the path to writing high-performing applications for the Web. The standard itself is still evolving, and its ecosystem is growing. Currently, this technology is receiving a lot of focus from the Cloud Native Computing Foundation (CNCF), with several projects under development.

Wasm’s design sits on two pillars: portability and security. The Wasm binary can run on any modern browser, even mobile devices. For security, Wasm programs run in a sandboxed, memory-safe VM. As such, they cannot access any system resources: they can’t change the filesystem or access the network or memory.

WebAssembly brings portability to the next levelLet’s say you want to build an application targeting many systems, e.g. Linux, Windows, and macOS. What are your options?

You could use a compiled language like C and build a binary for each target.

Compiler portability creates multiple executable files​

Or, if you can rely on having the appropriate runtime installed you could choose an interpreted language like JavaScript or one that compiles to bytecode like Java.

Code is compiled into an intermediate bytecode. This system relies on having a runtime environment, or VM, installed on the client.​

What if you have a container runtime in the client? In that case, you could build a Docker image for each platform type.

Code is compiled into platform-dependent images. A container runtime is required for clients, which pulls the correct image automatically.​

For Ruby developers historically, the only option was to distribute the code. That meant that users had to install the Ruby interpreter (or developers had to package the interpreter along with the application) to run the application.

Code is shipped directly to users, who must have the interpreter installed in their systems to be able to run it.​

All these mechanisms provide portability, but at a cost: you must build, test, and distribute many images. Sometimes, you must also ship a suitable runtime with the release or tell the user to install it independently.

WebAssembly (shortened as Wasm) takes portability to the next level: it allows you to build ONE binary and run it in any modern browser.

WebAssembly compiles into a low-level assembly that every modern browser can execute. As a result, the same Wasm binary can run, unmodified, on every platform (even mobile).​

The ability to run code at native speed has allowed developers to build sites like [Figma], and Google Earth or even run Vim in the browser.

Ruby adds support for WebAssemblyThe latest Ruby release ships with a Wasm port of the interpreter. Therefore, we can run Ruby code directly in the browser without the need for a backend.

As you can see in the example below, all it takes to get started with the Ruby Wasm port is a couple of lines. The script downloads ruby.wasm and instantiates the interpreter in the browser. After that, it takes the text of text/ruby type and feeds it into the WebAssembly program.

<html> <script src="https://cdn.jsdelivr.net/npm/ruby-head-wasm-wasi@0.5.0/dist/browser.script.iife.js"></script> <script type="text/ruby"> puts "Hello, world!" </script></html> You can confirm that Ruby is running from the browser, i.e. not connecting with a backend, by opening the developers’ tools. Here, you’ll find once ruby.wasm is downloaded, no further connections are needed.

Traditionally, JavaScript has been touted as the best language to learn because you have it everywhere. With WebAssembly, everyone can learn and experiment with Ruby using a browser. The output is printed in the developer’s console.​

You can even see the contents of ruby.wasm disassembled into text format in the “Sources” tab:

We can see the downloaded Wasm file in the browsers web developer tools.​

You can check out the Wasm port online at the Ruby playground.

king with the sandboxAs said, Wasm programs run in a sandboxed VM that lacks access to the rest of the system. Therefore, Wasm applications do not have access to the browser, filesystem, memory or the network. We’ll need some JavaScript code to send and receive data from the sandbox.

The following example shows how to read the output of a Ruby program and make changes to the page using the ruby-head-wasm-wasi NPM package:

<html> <script src="https://cdn.jsdelivr.net/npm/ruby-head-wasm-wasi@latest/dist/browser.umd.js"></script> <script> const { DefaultRubyVM } = window["ruby-wasm-wasi"]; const main = async () => { const response = await fetch( "https://cdn.jsdelivr.net/npm/ruby-head-wasm-wasi@latest/dist/ruby.wasm" ); const buffer = await response.arrayBuffer(); const module = await WebAssembly.compile(buffer); const { vm } = await DefaultRubyVM(module); vm.printVersion(); vm.eval(` require "js" luckiness = ["Lucky", "Unlucky"].sample JS::eval("document.body.innerText = '#{luckiness}'") `); }; main(); </script> <body></body></html> The same package can also run Ruby code inside a Node project, allowing you to mix Ruby and JavaScript on the backend. You’ll need to install the NPM package ruby-head-wasm-wasi for the example to work:

import fs from "fs/promises";import { DefaultRubyVM } from "ruby-head-wasm-wasi/dist/node.cjs.js";const main = async () => { const binary = await fs.readFile( // Tips: Replace the binary with debug info if you want symbolicated stack trace. // (only nightly release for now) // "./node_modules/ruby-head-wasm-wasi/dist/ruby.debug+stdlib.wasm" "./node_modules/ruby-head-wasm-wasi/dist/ruby.wasm" ); const module = await WebAssembly.compile(binary); const { vm } = await DefaultRubyVM(module); vm.eval(` luckiness = ["Lucky", "Unlucky"].sample puts "You are #{luckiness}" `);};main(); Running Ruby WebAssembly outside the browserWhile Wasm’s primary design goal is running binary code in the browser, developers quickly realized the potential of a fast, safe, and universally portable binary format for software delivery. Wasm has the potential to become as big a Docker, greatly simplifying application deployment for embedded systems, serverless functions, edge computing, or as a replacement for containers on Kubernetes.

Running a Wasm application outside the browser requires an appropriate runtime that implements the WebAssembly VM and provides interfaces to the underlying system. There are a few competing solutions in this field, the most popular being wasmtime, wasmer, and WAMR.

The Ruby repository provides a complete example for bundling your application code into a custom Ruby image.

Ruby WebAssembly LimitationsLet’s remember that this is all cutting-edge tech. The whole Wasm ecosystem is moving fast. Right now, Ruby Wasm has a few limitations which significantly limit its usability in big projects:

  • No thread support.
  • Spawning processes does not work.
  • No network support.
  • The garbage collector can create memory leaks.
  • Gems and modules are unavailable unless you build a custom Wasm image.

The future is brightWebAssembly opens a world of exciting possibilities. It allows Ruby developers to escape the backend. As tooling around WebAssembly improves, Ruby will be able to reach new frontiers: the browser is no longer off-limits, and there will be new opportunities to run Ruby on the edge and as serverless applications.

With the latest release, Ruby developers can begin experimenting with WebAssembly. It’s the first step, for sure, and there is much more work to do before we see complex Ruby applications running in with this technology.

Thanks for reading, and happy assembling!

The post Ruby Adds Support for WebAssembly: What Does This Mean for Ruby Developers? appeared first on Semaphore.

View Details

Continuous Integration (CI) pipelines enable software development teams to build and test code quickly and efficiently. However, the need for robust security measures becomes increasingly important. In this article, we interview Solutions Engineer Adib Saikali about his experience with CI pipeline security. From protecting sensitive data to detecting and preventing malicious code, Adib provides valuable guidance for ensuring the security of your CI pipeline.

Edited transcriptionAdib Saikali is a principal solutions engineer at VMware Tanzu, a platform for integrating cybersecurity in software development. As part of the services offered by his company, Adib has experience with role-based access control (RBAC) security, including securing CI pipelines. CI (Continuous Integration) pipelines automate the processes of writing code and are designed to catch errors or issues before they get to the production environment (CD or Continuous Delivery). CI implementation grants rapid feedback and easy identification and resolution of problems that can occur during development. Withal, CI pipelines are not free of security concerns, such as unauthorized access to sensitive data, the introduction of malicious code, and the potential for attackers to gain access to production systems.

Challenges when securing CI pipelines Over the years consulting companies, Adib has found two patterns regarding security in CI pipelines. First, how teams confirm if the security system code they wrote works with the real-world security system. When developers check out code locally and disconnected from the real security system, they might have difficulties running the application as intended. Hence, developer teams implement workarounds to try out the code without compromising the real system.

However, Adib has found that each team might develop a unique, not intuitive workaround to try out their code. As Adib puts it, “whenever you look at the code bases in these teams, it’s like ‘How do we do this? What are the unique ways of configuring security?’ ” Establishing a unique, centralized, and documented way of doing things will help organizations understand how to work with their code bases.

Secondly, Adib found that when writing tests, it can be difficult to know the identity of the user and how to handle complex permissions and workflow scenarios tied to security. Implementing this kind of test demands a complex permissions workflow.

To solve this last challenge, Adib recommends OpenID Connect (OIDC), an open standard for identity authentication and authorization. Built on top of the authorization framework OAuth 2.0, OIDC is an extra identity layer to verify the identity of a service end user using their existing credentials from a different service, without having to create new credentials. According to Adib, the great thing about OIDC is that “it allows us to define backend APIs, which are stateless,” meaning that “any state is stored in an external service like a database.”

With OIDC it is possible to create resource servers that demand whoever calls to have a certain token that will help them identify who is making the call. You can use OIDC to create a resource server that doesn’t have any information about how to log users but that merely reads whether or not they have a valid OIDC token and provides access depending on it.

According to Adib, the login should be handled by an OIDC server. The user experience could be, for example, in the case of a single page application, a user landing on the page and clicking on the login link, which would redirect them to the login server. In this regard, Adib advises against choosing client-side login. “The better way to do it is to in fact delegate that authentication to some sort of API gateway that’s in front of your stateless backend resource servers,” he affirms.

An API gateway is a server that acts as an intermediary between an application and a set of microservices. The API gateway is responsible for request routing, composition, and protocol translation. Moreover, it can also be used for authentication. As such, the API gateway can act as the single entry point for external consumers of the microservices and hide their underlying structure from the consumer, providing a consistent and simplified interface. The client (a native or single page app, or server-side rendered web app) will just HTTP redirect to the login server. The login server will display a login screen for users to log in. Upon successful login, they will establish an HTTP session with the API gateway. With the HTTP session, it is possible to track identities: on the client side, with the HTTP session ID, and on the server side, with the API gateway forwarding the backend services.

For implementing an API gateway, Adib recommends Spring Cloud Gateway, part of the Spring ecosystem. With Spring Cloud Gateway, “you can now actually do development the same way that it would be in production, except that you can program the OIDC server to have your standardized set of users to test different scenarios.” In this regard, for testing, Adib recommends creating fake users for the many plausible characters who would use that system and testing if their access limitations are working as intended.

Optimizing CI pipelinesWhen asked about any practical tips or information that can be shared about maintaining CI pipelines, Adib recommends keeping an eye open for new emerging standards, such as SLSA, a comprehensive security framework that includes a checklist of standards and controls to protect against tampering and maintain integrity in your projects. However, he also admits that the industry is five to ten years away from a holistic solution being widely available.

From the time being, Adib encourages us to try out everything, especially cutting-edge solutions, and see what fits best. As an example of this kind of technology, if you are you’re running your CI pipeline with coding containers, he encourages you to look at buildpacks.io, which offers both framework and runtime support for applications, and analyze your apps to identify necessary dependencies and properly set them up for execution on any cloud platform.

On the other hand, Adib recommends associating application projects with test fixture projects. Test fixtures are pre-configured environments to test software that allow developers to fix states. Test fixtures “make all the tests simpler to write by having them all leverage common helper classes,” Adib explains. In this way, test fixtures become “common code libraries’ ‘ that can be as handy as any internal utility library.

The challenge of writing test fixtures code, says Adib, consists of being “able to create things where you can produce these test errors that are hard to reproduce.” For this reason, Adib recommends maintaining three types of source code for each project: the project code, the test fixture code, and the test code. The test code you produce will depend on the other two types of code. Withal, Adib reminds us that creating this environment can be as challenging, if not more, than writing the actual application code: setting up the right security contacts, getting dependencies like the database or the application, and simulating error conditions.

Lastly, to simulate error conditions, Adib also suggests implementing a way of testing databases. “A lot of the time our state is stored in some sort of database and, depending on what’s in the database, your test may pass or it may fail,” he points out. He recommends using Java library Testcontainers and running JUnit tests through it. You can spin up containers for the specific version of the database before running the test. The containers are then thrown away after the test has finished ensuring that each test starts with a clean environment and eliminates the chance of interference between tests.

Leveling up the security culture of your teamOn a skill level, Adib encourages developers to get into cybersecurity. He argues that while it might seem intimidating at first, they should start by learning to separate “what you need to know from what you don’t need to know”. In this regard, he advises learning three kinds of technologies:

  • Cryptography. Specifically, the basics, including hash functions, public key cryptography, and symmetric key cryptography.
  • Digital certificates. Mainly, X.509 certificates. “X.509 certificates are super important to understand because they’re used everywhere, and they’ve been used everywhere for decades, and they’re going to continue to be used everywhere for decades more,” Adib affirms.
  • Transport Layer Security (TLS). He understands that “TLS is everywhere and we should be doing mutual TLS everywhere, whichever way we do it.”

The next step after learning about these technologies, says Adib, is learning how to deal with two security concerns. First, on how users log in and what technologies come into place. Adib advises getting used to the OIDC protocol, which works with all programming languages. He also considers it essential to pick up WebAuth for logging in without a password.

The second security concern is how to secure API call chains. An API call chain is a series of calls made to different APIs or services in a specific order, with the first call at the top of the chain and the last call at the bottom. These calls are made to perform tasks or access data, and the chain of calls together forms the overall program functionality. According to Adib, “there isn’t an industry-standard way to do it”, so you are forced to judge by yourself among the employed methods, such as API gateways and service mesh, an architecture that can be implemented to control and monitor the communication between different services.

The bottom lineCurrently, Adib is writing a book on cybersecurity named Securing Cloud Applications. His approach is writing a book about cybersecurity not for cybersecurity experts but developers. “A lot of the books I went through were very much written by security people for security people, so this book represents the thing I wish I had when I started learning about security years ago,” he confesses. The book follows the teaching of the cybersecurity fundamentals he mentioned earlier, including cryptography, digital certificates, TLS, user login, and securing the call chain, as well as Kubernetes. You can check his book progress here.

Adib understands that one of the things that differentiate his book is that it does not demand doing math. Instead, it features sample applications. “The idea is to help developers understand security by running a sample app that lets them see the security protocols and actions,” he says.

Follow Adib on Twitter and LinkedIn and check his website for finding his posts, talks, and workshops. You can also find Adib’s past talks and presentations on Youtube.

The post Adib Saikali on Protecting Your Software Development Workflow appeared first on Semaphore.

View Details

You never get a second chance to make a first impression. So how do you make sure it’s a good one? This matter is constantly in the mind of release managers and product owners, because they know that once a product is set loose into the world all bets are off.

Although it may be tempting to release a product or new feature as soon as it is functional, users expect (and deserve) quality and polish. Before even reaching the beta test stage, a lot of non-technical work must be done: documentation must be written, security must be assessed, and business objectives must be established. These are some of the things we have learned about software release at Semaphore.

Project stagesFrom idea to release, we can break a software project into roughly three stages:

  • Design and development: comprises everything from idea to MVP or working feature. We’ve discussed design patterns in other blog posts, so I won’t spend too much time on that at this point. All I will say is that there is no shortage of practices to help us uncover good designs, like writing acceptance criteria and following Test-Driven Development, Behavior-Driven Development, or Domain-Driven Development methodologies.
  • Tech preview: the sharp blade of the reality check. We bring a few users to get their early feedback so we can make improvements and reduce uncertainty at the release.
  • Release: also known as General Availability (GA). In this last stage, we make the release available to all users.

The stages are separated by checkpoints. Of which we have two kinds Definition of Ready and Definition of Done:

  • Definition of Ready (DoR): these are all required tasks before users can access the system or experience the feature.
  • Definition of Done (DoD): consists of all the follow-up tasks closing a stage, like gathering user feedback, completing outstanding tickets, or doing a retrospective.

We cannot enter a new project stage or consider it done until all the tasks in DoR and DoD are ticked off.

The ready checkpoint (DoR) dictates if users can be allowed into our system. Thus, the technical preview can begin only once we meet the DoR. It does not end until we’ve completed all the tasks in the DoD. The same thing happens during GA release.The technical previewThe technical preview is an opportunity to get the user’s feedback before the final release. We must bring them into the mix before evaluating whether a feature or product works. During the preview stage, we deploy the latest release in production and use feature flags to control who can see the previewed feature.

We can deploy the latest release in production and use feature flags to select which users access the previewed feature.Most users understand that they may find some kinks and will be happy to help us sort them out. Therefore, we can start the tech preview even if the application is not perfect (hint: it never will be).

That being said, there are some precautions we need to take before any user is let into our systems. Here’s where the DoR for the technical release comes into focus.

Definition of Ready for the technical previewLet’s say we’re shipping a new feature in our product. Early feedback from real users is essential for its success. We can reach out to users who may benefit from the feature or be interested in testing it out.

How do we know when we’re ready to invite users to check it out? We define a list of tasks and conditions that must happen before we can define our system as ready. These are tasks that are required to be complete for every feature before it can be shown to the users:

  • Performance metrics: every new component shipped should be tracked and added to the monitoring dashboard to observe its performance and stability. Semaphore, for instance, monitors the four golden signals (latency, traffic, errors, and saturation).
  • Utilization metrics: we need to make sure if the new features are being used and how.
  • Audit Logs: shipping anything to production that’s not covered in audit logs is a security issue. Every event should be logged to have full audit compliance.
  • Security Assessment: we must ensure the new components don’t have any critical vulnerabilities. This is important for two reasons. First, to protect users from security exploits. And second, to comply with any security standards the company follows, such as ISO 27001.
  • Deployment to production: the release should be deployed to production. This can be the primary productive environment or a staging copy that’s as close as possible to the real thing.
  • Preliminary documentation: the users will need instructions about what changed and how to use it. It doesn’t need to be full-blown documentation yet.

The list can be expanded to include more items as needed.

Once we have our DoR defined and fulfilled, we’re ready to start the technical preview. The feature passes to the “in-review” state, and we can begin the feedback-gathering process that may last weeks or months.

💡 Instead of technical previews, the engineering team at Semaphore used to have two kinds of betas: public and private, which had different processes and requirements. Having two sets of similar-but-not-quite-the-same DoRs and DoDs ended up being unnecessarily complicated. So, the processes were streamlined into one. Users are invited to the technical preview based on needs and interest in a given feature.

Definition of Done for the technical previewThe technical preview ends when we’ve gathered enough feedback and refined the system to the point we consider it ready for release.

Before we can close the technical preview, however, there are some tasks to complete, which will depend on the DoD for our project.

The items in the DoD bucket vary, as they will change from team to team or from company to company. For Semaphore, the list includes:

  • Gathering feedback: the whole point of this exercise is to gather actionable feedback from the users. Feedback lets us better incorporate the voice of the customer into the system. It lets us find bugs and improve usability.
  • Cleaning up any remaining tasks: address and close outstanding tickets. Ensure all the problems and feedback obtained during the technical preview have either been fixed or have mitigation plans in motion.
  • Communicating internally: everyone involved in supporting production should be notified about any problems found during the technical preview (and their solutions).
  • Setting up alerts: we need to be aware of a problem in production as soon as it happens. This includes configuring Slack/Teams alerts, setting up pagers, and ensuring that the on-call personnel are on the same page.

General availability releaseThe moment of truth we’ve prepared for so hard is close at hand. Users will not be as forgiving during a general availability (GA) release as during a tech preview. Problems might disrupt their work. That’s why we went to such lengths to minimize the chance of problems in the release. However, no amount of preparation will guarantee a successful release. So, we need to prepare for any issues that might arise.

Definition of Ready for releaseThe software might be ready to release, but that doesn’t mean we are ready for the release. So, we must define and fulfill the GA release DoR checklist before going into the next step.

As always, every project has different needs. As a guideline, here’re are some of the tasks that are considered necessary at Semaphore:

  • Writing the public docs: public docs must be updated with the details of the new feature. In addition, it should be clear which plans have support for it.
  • Skilling up internally: everyone in the company, especially support and customer-facing staff, should be briefed on the upcoming changes.
  • Setting KPIs: based on technical preview utilization, we can set business objectives and key performance indicators (KPI) for the feature in question. This may include new revenue expected, utilization metrics, or feature adoption.
  • Writing a change log: the change log should include everything that has changed since the last release.
  • Setting up alerts: we need to be aware of any problem with the new features shipped as soon as it happens. This includes Slack/Teams alerts and pager notifications for the on-call personnel.
  • Planning a marketing strategy: the marketing strategy communicates the changes and generates user interest. A good campaign can bring new customers or convert free users into paid ones.

Definition of Done for GAWith some luck, the release went smoothly, and users are happy with the new features. What now? Can we move to the next project? Not yet. This project is not done until all items in the DoD are checked off.

What’s typically left at this point is doing some cleanup work and tidying up loose ends:

  • Clean leftover tickets: after shipping the new features to GA, there might be some remaining low-priority tasks. They should be addressed or scheduled to be worked on later.
  • CLI and API Support: provide support to the new feature via public APIs or the CLI tools, because this makes it easier for users to access resources from the terminal and integrate them into their custom tooling. If this support is integral to the new feature, we can move this item to the DoR checklist.
  • Do the project retrospective: The retrospective is one of the most critical phases of a project. It’s a space for the team to discuss what went well and what can be improved for the next project. Also, it allows the team to identify potential pitfalls early and resolve conflict areas.

ConclusionIt’s a mistake to think that the hard part of software development is coding. In reality, the real challenge comes when you open the floodgates and users pour in. Unless everything is prepared, issues can compound and overtake any engineering team. Having a formal definition of what constitutes being ready and done serves us to avoid rushing through a release.

Thanks for reading, and happy releasing!

The post Release Management: Is Your Product Ready for Success? appeared first on Semaphore.

View Details

I started studying Ruby in 2012. I’m not quite a dinosaur, because when I began my journey, there were already experienced Rubyists out there. Nonetheless, this was a time when Ruby was a proudly dynamic language and Rails 3 was all the rage.

Don’t get me wrong, at present, Ruby still is a proudly dynamic programming language! In the last few years, however, Matz and the community have started to entertain the idea of adding some level of static type checking to the language. One of the pioneers in this wild west phase was Stripe’s Sorbet. On the 25th of December, Ruby 3.0 finally brought us an official answer to this in the form of RBS, a standard type language for Ruby.

It’s normal during the career of a developer to move away – at least temporarily — from what once was their main language. That was the case for me just before the release of Ruby 3.0 and I suppose that this has also happened with many other folks. I’m writing this article having us older Rubyists as the primary audience, but if you’re a beginner, however, don’t shy away: I’m certain this blog post will also have value for you, if you aren’t yet familiar with RBS, TypeProf and other citizens of this new ecosystem of static analysis related tools in Ruby.

In this article, I’ll explain what RBS is (and what it is not). I’ll also show how to use it step by step and conclude this blog post with my thoughts on this trend and how to best use static type checking in real-world Ruby programs.

All of the code shown in this blog post is available in a companion Git repository. You can find it at https://github.com/alexbrahastoll/semaphore-1-ruby-3-rbs-typeprof

RBS: what it is and what it is notRecently, when I began programming a lot in Ruby again, I was naturally curious about the new features introduced in 3.0. At first, I thought that RBS, besides being able to provide typing for your Ruby program, could also be used to perform static type checking. This is actually not the case! With .rbs files you can specify which types are used where, but RBS doesn’t concern itself with the meaning of each type (i.e. their implementation) nor with providing type checking (i.e. with analyzing your program without running it and checking whether types are actually used in the places they are supposed to). To sum all this up, RBS is only a type language, not a complete type checker. Nonetheless, RBS is an important official addition to the Ruby ecosystem, as it is the standard type language, thus avoiding the proliferation of third-party solutions and the fragmentation of the user base and complementary tools. By the way, RBS is supposed to stand for Ruby Signatures, although I didn’t find information on the exact meaning of the acronym on the official Ruby site, only loose references on third party sources across the Web.

If we think of RBS as a project, it’s also concerned with providing type signatures for both the core and standard libraries. As you’ll soon see, this is of the utmost importance. As even a simple Ruby program will use the core and standard libraries, it’s fundamental to have these typed, otherwise a type checker wouldn’t be able to do much.

Furthermore, to be completely honest, RBS does come with a tool to dynamically check for type errors by injecting itself into a test suite, but this is not static type checking and is dependent on both a test suite being available and actually decently exercising the system under test. More info on how to use this tool is available at RBS’ GitHub repository.

Our first steps using RBS and TypeProfTo showcase RBS, let’s use a small program that I wrote to calculate the number of working days in a given period. As inputs, the program receives a start date, an end date, and an array of dates representing all the days off for the period.

At first, we will have a very simple and more verbose implementation of this working days calculator. The idea is to test how the different tools in the ecosystem can handle a not so idiomatic – but very straightforward — implementation. Then, we will move to a more concise and idiomatic implementation of the same program and repeat the process.

Here’s how our program looks like:

```

working_day_calculator.rbrequire 'date'class WorkingDayCalculator attr_accessor :period_start, :period_end attr_reader :days_off def initialize @days_off = [] end def add_day_off(date) @days_off << date end def calculate_working_days ((period_start..period_end).to_a - days_off).length endend

``` RBS comes with a tool able to generate signatures from Ruby source code. This tool is called TypeProf. The signatures it generates, however, are only a starting point and generally need to be manually enhanced in order to actually have any value in practice.

To generate signatures for our calculator, we run the following command:

typeprof working_day_calculator.rb TypeProf analyzes our program and then outputs the following:

```

TypeProf 0.21.3# Classesclass WorkingDayCalculator attr_accessor period_start: bot attr_accessor period_end: bot attr_reader days_off: Array[untyped] def initialize: -> void def add_day_off: (untyped date) -> Array[untyped] def calculate_working_days: -> untypedend

`` We can then copy and paste what really matters into a new file,working_day_calculator.rbs`, which is the corresponding type signature file for our calculator.

Before moving on and trying to have some type checking done for us, let’s see what an RBS file looks like. It’s out of the scope of this post to provide a complete reference to its syntax, but let’s at least go through the basics. A complete reference is available at RBS’ GitHub repository.

As is clear from the RBS snippet above, the type language’s syntax is very similar to the syntax of Ruby itself. In the first two lines, we indicate that our WorkingDayCalculator class has methods for reading and writing the period_start and period_end instance variables. TypeProf was not able to figure out the types of these variables, so it marked them as bot, which is short for bottom – bot is a subtype of all types.

Then, TypeProf detected that we have a reader for @days_off. Furthermore, it was also able to figure out that @days_off is an array! Since it couldn’t determine which type or types are to be held inside @days_off, it generated an Array[untyped]signature for this instance variable.

Finally, we have the signatures for the methods of WorkingDayCalculator. As an example, let’s take a look at the one generated for #add_day_off. TypeProf correctly identified that we expect one argument called date, but it was unable to determine the type of date, so it marked it as untyped. TypeProf detected that we return an array for this method (which is actually the instance variable @days_off), however, this is not that important. Here, the return value of #add_day_off is a consequence of Ruby implicitly using as return value the last evaluated expression of a method when an explicit return is not present or reached.

A simple test script that uses our calculatorIn order for us to be able to type check our code in a more realistic manner, it’s better if we have code that uses the WorkingDayCalculator class. A simple script that gives some valid inputs to an instance of the calculator and then calls #calculate_working_days will serve us perfectly. Here’s our test script:

```

working_day_calculator_test.rbrequire_relative './working_day_calculator'calc = WorkingDayCalculator.newcalc.period_start = Date.new(2023, 1, 1)calc.period_end = Date.new(2023, 1, 31)calc.add_day_off(Date.new(2023, 1, 1))puts calc.calculate_working_days # Expected: 30

``` In the next section, we’ll finally start experimenting with type checking. The script above will be our playground and we will change it and introduce type errors on purpose to see some actual type checking in action.

You reap what you sow: type checking typed Ruby code with SteepNow that we have very basic signatures for our code, we should be able to start type checking it. As explained before, these batteries are not included with RBS’ package. There are, however, multiple tools in the Ruby ecosystem already leveraging RBS. In this blog post, we’re going to use Steep. Available as a gem, Steep is a type checker created by Soutaro Matsumoto – a Ruby Core committer.

Steep requires a configuration file named Steepfile to be present in the directory from which the included CLI is run. A basic Steepfile can be generated with the command steep init. Look up Steep documentation for more information on the anatomy of a Steepfile. Here’s how ours looks like:

```

SteepfileD = Steep::Diagnostictarget :lib do signature 'sig' check 'working_day_calculator_test.rb' configure_code_diagnostics(D::Ruby.strict)end

`` Now, we can proceed and runsteep check` to initiate the type checker. After a few seconds, you should see an output similar to the one below:

```

Type checking files:..................................................................................No type error detected. 🧉

``` Before you get all up in arms, I’m going to say that I agree that this is not impressive, but at least things seem to be working! Can Steep detect type errors with our current RBS files? Let’s check and see by introducing an error to our test script on purpose:

```

working_day_calculator_test.rbrequire_relative './working_day_calculator'calc = WorkingDayCalculator.newcalc.period_start = Date.new(2023, 1, 1)calc.period_end = Date.new(2023, 1, 31)# calc.add_day_off(Date.new(2023, 1, 1)) # original, correct usage of #add_day_offcalc.add_day_off('BOOM') # incorrectly passing a String to #add_day_offputs calc.calculate_working_days # Expected: 30

`` Now, when we runsteep check` again, we should see a type error, right? Unfortunately, that is not the case:

```

Type checking files:..................................................................................No type error detected. 🫖

`` This might seem strange at first, but is no surprise when we check more carefully our RBS file for theWorkingDayCalculatorclass. Since the signature of#add_day_offexpects an *untyped* argument, Steep is not able to determine that passing aString` to the method should constitute a type error.

For our current signature file to be useful, we have to manually enhance it. Let’s indicate, where appropriate, that dates are expected to be a Date type. Here’s our enhanced RBS file:

```

working_day_calculator.rbsclass WorkingDayCalculator attr_accessor period_start: Date attr_accessor period_end: Date attr_reader days_off: Array[Date] def initialize: -> void def add_day_off: (Date date) -> Array[Date] def calculate_working_days: -> untypedend

``` Now, when we run Steep again, here’s its output:

```

Type checking files:..............................................................................F..Fsig/working_day_calculator.rbs:2:30: [error] Cannot find type Date│ Diagnostic ID: RBS::UnknownTypeName│└ attr_accessor period_start: Date ~~~~working_day_calculator_test.rb:10:7: [error] UnexpectedError: sig/working_day_calculator.rbs:2:30...2:34: Could not find Date│ Diagnostic ID: Ruby::UnexpectedError│└ calc = WorkingDayCalculator.new ~~~~~~~~~~~~~~~~~~~~~~~~

`` Note: if you want to experiment with the code shown above, check out thev1-step1` tag in this article’s companion repository.

It’s nice to see some errors, but not quite the ones we were expecting, right? Remember that I explained above that part of the efforts of the team responsible for creating and evolving RBS is to provide signatures for Ruby’s core and standard libraries? The errors we just saw are due to the fact that we are using a type (Date) that Steep doesn’t recognize. Since the Date class is part of Ruby’s standard library, signatures for it are available. A simple change to our Steepfile is all that is needed:

```

SteepfileD = Steep::Diagnostictarget :lib do signature 'sig' check 'working_day_calculator_test.rb' library 'date' # Instructs Steep to load the signatures for the date library. configure_code_diagnostics(D::Ruby.strict)end

`` Heads up: library names should follow the naming convention used to load them! Since werequire 'date', we must uselibrary 'date'(usinglibrary 'Date'` will result in an error).

After making this adjustment to our Steepfile, we finally see our first static detection of a type error in Ruby code:

```

Type checking files:....................................................................................Fworking_day_calculator_test.rb:13:17: [error] Cannot pass a value of type ::String as an argument of type ::Date│ ::String <: ::Date│ ::Object <: ::Date│ ::BasicObject <: ::Date││ Diagnostic ID: Ruby::ArgumentTypeMismatch│└ calc.add_day_off('BOOM') ~~~~~~~~~~Detected 1 problem from 1 file

`` Note: if you want to experiment with the code shown above, check out thev1-step2` tag in this article’s companion repository.

Improved Ruby, improved RBSNow, let’s improve the implementation of our calculator. To make it much more succinct and idiomatic, let’s throw away all other methods with the exception of #calculate_working_days. For the period under consideration, we now expect a Range of dates. For days_off, an array of ranges of dates and/or dates must be given. With these modifications, we end up with some much more rubyesque code, listed below:

```

working_day_calculator.rbrequire 'date'class WorkingDayCalculator def calculate_working_days(period, days_off) flattened_days_off = days_off.map { |date| date.class == Range ? date.to_a : date }.flatten (period.to_a - flattened_days_off).length endend

`` UsingTypeprof`, we can generate a starting point for our RBS type signatures:

```

working_day_calculator.rbsclass WorkingDayCalculator def calculate_working_days: (untyped period, untyped days_off) -> untypedend

`` Note: if you want to experiment with the code shown right above, check out thev2-step1` tag in this article’s companion repository.

We know that the generated RBS will not be good for detecting any type errors. So, let’s improve it and add signatures that perfectly describe this second version of the WorkingDayCalculator class:

```

working_day_calculator.rbsclass WorkingDayCalculator def calculate_working_days: (Range[Date] period, Array[Range[Date] | Date] days_off) -> Integerend

`` Our improved RBS file nicely illustrates the expressiveness of Ruby type signatures. TheRangetype allows for all kinds of ranges (Integer,Dateand so on). If Ruby were a typed language, this would mean that theRangeclass would have a generic data type as a parameter in its definition (something likeRange[T]). In our case, we want to indicate that we expect a specific type of range, specifically one of dates. We can do so by designating the type ofperiodasRange[Date]`.

Regarding the days_off parameter, here we need to indicate that we expect an array that can be composed of any combination of ranges of dates and date objects themselves. That is to say that the type of days_off‘s array is a union type or, in other words, a type representing multiple possible types. We can express this concept as the following: Array[Range[Date] | Date].

Finally, we should also indicate that #calculate_working_days has a return type. The value returned by our method will always be an integer, which we represent with -> Integer.

As we did in the first part of this article, we will have a file that uses WorkingDayCalculator so we can purposefully have examples of incorrect usages. These type errors are explained in the gist below alongside the code itself. We expect Steep to be able to detect them.

```

working_day_calculator_test.rbrequire_relative './working_day_calculator'calc = WorkingDayCalculator.newperiod_start = Date.new(2023, 1, 1)period_end = Date.new(2023, 1, 31)days_off = [Date.new(2023, 1, 1), Date.new(2023, 1, 29)..Date.new(2023, 1, 30)]# correct! prints "28"puts calc.calculate_working_days(period_start..period_end, days_off)# type error, arg 1 must be a range of datescalc.calculate_working_days([], days_off)# type error, arg 2 must be an array composed of dates and / or ranges of datescalc.calculate_working_days(period_start..period_end, Date.new(2023, 1, 1))# type error, arg 2 must be an array composed of only dates and / or ranges of datescalc.calculate_working_days(period_start..period_end, [ Date.new(2023, 1, 1), '2023-01-15', Date.new(2023, 1, 29)..Date.new(2023, 1, 30)])

`` Now, we can runsteep checkand see if it detected the type errors present inworking_day_calculator_test.rb`. Here’s its output:

```

Type checking files:....................................................................................Fworking_day_calculator_test.rb:12:28: [error] Cannot pass a value of type ::Array[untyped] as an argument of type ::Range[::Date]│ ::Array[untyped] <: ::Range[::Date]│ ::Object <: ::Range[::Date]│ ::BasicObject <: ::Range[::Date]││ Diagnostic ID: Ruby::ArgumentTypeMismatch│└ calc.calculate_working_days([], days_off) ~~working_day_calculator_test.rb:15:54: [error] Cannot pass a value of type ::Date as an argument of type ::Array[(::Range[::Date] | ::Date)]│ ::Date <: ::Array[(::Range[::Date] | ::Date)]│ ::Object <: ::Array[(::Range[::Date] | ::Date)]│ ::BasicObject <: ::Array[(::Range[::Date] | ::Date)]││ Diagnostic ID: Ruby::ArgumentTypeMismatch│└ calc.calculate_working_days(period_start..period_end, Date.new(2023, 1, 1)) ~~~~~~~~~~~~~~~~~~~~working_day_calculator_test.rb:18:54: [error] Cannot pass a value of type ::Array[(::Date | ::String | ::Range[::Date])] as an argument of type ::Array[(::Range[::Date] | ::Date)]│ ::Array[(::Date | ::String | ::Range[::Date])] <: ::Array[(::Range[::Date] | ::Date)]│ (::Date | ::String | ::Range[::Date]) <: (::Range[::Date] | ::Date)│ ::String <: (::Range[::Date] | ::Date)│ ::String <: ::Range[::Date]│ ::Object <: ::Range[::Date]│ ::BasicObject <: ::Range[::Date]││ Diagnostic ID: Ruby::ArgumentTypeMismatch│└ calc.calculate_working_days(period_start..period_end, [ ~Detected 3 problems from 1 file

`` As shown in the snippet above, Steep was able to detect all type errors present inworking_day_calculator_test.rb`. Quite an impressive result, if you ask me! This shows that, if one puts the effort into creating good type signatures, the tool can indeed detect complex kinds of type errors.

Note: if you want to experiment with the code shown right above, check out the v2-step2 tag in this article’s companion repository.

Type Checking in Ruby: the great and the not so greatRuby 3 introduced RBS, an official type language for Ruby. Alongside community tools such as Steep, it affirmed that Ruby 3 has now become a programming language with gradual typing. This is positive because it allows teams to increase the reliability of their products and stay on par with the trend of gradually-typed languages (as you probably know, many developers care a lot about being fashionable).

It’s certainly good to have more rather than less tools to help us make our Ruby programs more reliable. Does this mean that typing in Ruby solves all reliability problems software can have? If that was the case, it wouldn’t be necessary to write automated tests when programming in typed languages, so the answer here is obviously no. Does this mean all Ruby projects should now have RBS type signatures for 100% of the codebase? Also, I think that would defeat the great strength of Ruby as a highly dynamic language, so the answer is also no in my opinion.

As an experienced Ruby developer, I see RBS type signatures being used mainly in two scenarios. First, in larger teams working on mature codebases. In such an environment, it’s plausible to imagine that adding type signatures at least to the more complex or mission critical parts of the codebase might result in an increase in overall reliability. The second case in which I believe that types might be useful is for programs, or parts of programs, for which it’s hard to write and maintain automated tests for. Again, investing in type signatures in these cases may result in increased product reliability.

What about you? Has this blog post made you curious about RBS, Typeprof and Steep? If so, I highly recommend that you check the resources mentioned throughout this article and do your own experiments using gradual typing in Ruby!

The post Ease Into Ruby 3’s Static Typing Powers With RBS, TypeProf and Steep appeared first on Semaphore.

View Details

As organizational dependence on the cloud grows, so do the challenges of implementing and managing security strategies. Strategies that may have been effective for traditional, on-premise networks are generally inadequate for cloud infrastructure. This is because cloud networks are becoming increasingly complex: organizations these days rarely rely on just one cloud service. Instead, businesses deploy many different services from multiple vendors, often relying on a mix of public and private cloud providers to support their operational goals. The complexity of the cloud demands that the way we approach cloud security must be layered and strategic. When developing a robust cybersecurity strategy for your cloud network, it’s important to consider three main pillars to maximizing security potential:

  • Hybrid and multi-cloud environments
  • Seamless functionality
  • Branch office edge protection

Hybrid and Multi-Cloud EnvironmentsBoth hybrid and multi-cloud refer to cloud deployments that utilize more than one cloud. Where they differ is in the types of infrastructure they use. A hybrid cloud refers to deployments that utilize two or more different types of clouds, including a private cloud along with one or more public cloud services. Alternatively, a multi-cloud environment always includes more than one public cloud service. Multi-cloud environments don’t always include a private cloud, but when they do, they can be considered both a multi-cloud and a hybrid cloud.

Since both hybrid and multi-cloud use multiple cloud computing service providers, businesses are able to maximize the value that the cloud offers with highly flexible services. Businesses don’t need to lock into one specific vendor or contract. Instead, they can choose from a variety of solutions in order to best suit the needs of their organization.

Another major benefit of using a hybrid or multi-cloud environment is risk mitigation. Service outages can be catastrophic, often costing organizations thousands of dollars per second of downtime. Spreading services across multiple cloud providers means that even if one provider goes down, you still have alternative resources at your disposal.

Whether or not you choose hybrid or multi-cloud depends on the needs of your organization. A hybrid cloud may be ideal for companies operating within countries which restrict the storage of sensitive data on public hyperscalars. Being able to take advantage of the scale and flexibility of public clouds while observing the legal requirements of PII data storage is a real competitive advantage.

Securing your Hybrid and Multi-Cloud Environments: Best Practices1. Address Interoperability

Interoperability allows businesses to fully take advantage of the flexibility and scalability of their cloud environment by integrating new technologies with existing services. While this is typically seen as a major benefit, there are security considerations that must be addressed. Bad configuration, poorly implemented authentication, and outdated services can lead to security vulnerabilities that may put your organization at risk.

  1. Understand how the shared model works

It’s important to understand how the shared model is applied to you and your cloud providers. Typically, cloud providers are responsible for the security of their own infrastructure, but often provide customers with additional security features to protect their data. How your data is used in their infrastructure is usually the responsibility of your organization. For example, any third-party software your organization deploys on the cloud is your organization’s responsibility to keep patched and otherwise hardened.

  1. Choose your cloud vendors carefully

It’s important to choose your cloud vendors carefully and have a thorough understanding of the products, services and security policies of each. Having a comprehensive understanding of the products and services being used is essential to maintaining a secure environment.

  1. Harden your applications

Hardening your applications is essential to maintaining a secure environment. A vulnerability and exposure (VnE) manager can be critical to achieving this, as it regularly monitors your applications for threats and vulnerabilities.

  1. Backup your data

In today’s age, ransomware attacks are more common than ever, costing businesses millions each year. Mitigate the risks of ransomware attacks by maintaining up to date and secure backups of your data.

  1. Monitor for change

The security of your services should be continuously monitored and assessed. Updates, configuration changes, the addition of new services, or any other changes to your system can create new security vulnerabilities that must be addressed immediately.

Seamless FunctionalityOne of the major challenges for maintaining an effective cybersecurity strategy is implementing security solutions that are simple and easy to use. One-time passwords or push notifications for password-based multi-factor authentication, for example, can be frustrating for users and reduce productivity. With the increasing complexity of cloud environments, it’s essential for organizations to adopt seamless security solutions that allow users to access cloud services in your environment with minimal disruption to workflow.

Below are some examples of seamless security controls:

Single Sign-on (SSO): single sign-on allows users to access multiple applications with one set of credentials. This eliminates the need to sign in to each individual application as well as the need for multiple username and password combinations.

Biometric Authentication: biometric authentication uses an individual’s unique physical characteristics as part of the authentication process. It is used as a type of multi-factor authentication, most often coupled with a username and password. Examples of biometric authentication include fingerprint or facial recognition. The benefit of using biometric authentication means that users don’t need to enter additional passwords or one-time passcodes.

Automation: automation is becoming an increasingly central component of a robust cloud security framework. Automation allows businesses to automate security related tasks, such as automatic updates, automatic vulnerability scanning, and more. The benefits of automation mean that certain tasks can be taken care of by automation tools instead of requiring user intervention.

Branch Office Edge ProtectionOften enterprises require on-premise branch offices in order to comply with location requirements, compliance, or other data privacy concerns. While connecting a branch office to the cloud poses additional security risks, steps can be taken to maximize the security of your branch offices.

Understanding the risksThe nature of the edge poses a big security challenge for organizations and an appealing target for malicious actors. For one, edge projects tend to be populated with a high number of IoT devices and supporting infrastructure, and as a result generate a massive amount of data. This makes the edge a prime target for criminal hacker organizations. Another major security challenge is the sheer scale of deployment locations. The edge is often spread across hundreds or even thousands of locations, ranging from anything like warehouses, factories, retail stores and more. The vast number of edge locations poses the challenge of ensuring that every location in the network is secured.

Branch office edge security best practices:1. Edge must be integrated into your security strategy: each edge node requires the same level of security, visibility and redundancy as your central data center. This should include multiple layers of protection such as endpoint protection, multi-factor authentication (MFA), malware protection, end-user training, and more.

  1. Monitor for change: automated monitoring tools can aid in alerting you of system changes and potential security threats, helping you respond to potential security threats as soon as they appear.

  2. Make patching a priority: aggressive patch management is recommended to maximize the security of your branch office edge. This can be done using centralized configuration management and monitoring tools.

Securing IoT with SD-Branch SolutionsIoT devices are increasingly becoming a target for hackers. IoT devices not only increase the network’s potential attack surface, but they also often contain security vulnerabilities due to factors such as poor code, built-in backdoors, and limited memory and CPU. This is where SD-Branch comes in. SD-Branch extends native security that is built into your Secure SD-WAN into the branch network, providing organizations with additional security for IoT devices. SD-Branch solutions include three components:

  1. Network edge protection: your next-generation firewall must extend security from the SD-WAN connection to access controllers in order to secure all inbound and outbound IoT traffic.

  2. Device edge protection: integrated network access control (NAC) solution should be used to identify, segment, and to apply policy to all IoT devices.

  3. Access edge protection: access points must secure IoT traffic moving through the branch network

By incorporating branch office edge protection into your overall strategy, you can effectively manage your cloud security to ensure the safety of your data in a seamless, efficient way.

SummaryModern cloud environments are becoming ever more complex, and so too are the security challenges that come along with it. Hybrid and multi-cloud environments that integrate many services and connect a large number physical locations and IoT devices require a sophisticated security framework to keep up with a constantly evolving cyber landscape. With the right cybersecurity strategy and tools, you too can maximize security potential in your cloud environment.

The post 3 Pillars to Maximizing Security Potential in Complex Cloud Environments appeared first on Semaphore.

View Details

In the Spring of 2022, an anonymous Google employee published a blind article attacking some of Google’s technical interview processes.

More specifically, it attacks the ‘know it all’ attitude of interviewers, who often lack practical experience, and the complexity of questions that can sometimes take Google’s own employees weeks to solve.

Source: UnspalshThe post started a lively discussion among developers, who started sharing their bad interview experiences.

While Google’s hiring process has attracted much attention over the years, this problem affects many companies that struggle to provide an interview process that is both effective and straightforward.

This article will detail some of the best practices in the tech industry to help you improve your hiring process.

Source: UnsplashThe interview process starts long before the first CV lands in your inbox, and it includes an essential part of the process — defining the role and its requirements.

A well-defined role with relevant requirements will encourage suitable candidates to apply while dissuading candidates without the necessary experience from sending their CVs.

Source: UnsplashSebastian Ramirez is the creator of FastSPI – a modern, fast (high-performance) web framework for building APIs with Python.

A few years ago, he shared the following tweet:

I saw a job post the other day. 👔

It required 4+ years of experience in FastAPI. 🤦

I couldn't apply as I only have 1.5+ years of experience since I created that thing. 😅

Maybe it's time to re-evaluate that "years of experience = skill level". ♻

— Sebastián Ramírez (@tiangolo) July 11, 2020

Ramirez’s tweet points to two common problems with technical job requirements:

  1. Companies tend to mix between essential and nonessential requirements for the role.
  2. Some common qualifications are poor indicators of the candidate’s success in his role.

  3. Essential vs. Nonessential requirements:The role should distinguish between essential and nonessential requirements.

To determine which requirements are essential, ask yourself two questions:

  1. Can a candidate pass the interview without possessing the required qualification? i.e., if the technical test is written in Python, candidates must have prior Python experience to pass.
  2. Would you accept a candidate that passed the interview process but does not have that qualification?

If the answer to both of these questions is yes, this is a nonessential requirement.

  1. Choosing the correct qualificationsSource: UnsplashPrior education, years of experience in a given programming language, and even GPA scores are easily accessible and are often used to screen candidates.

Yet, many large tech companies and government organizations such as NASA are moving away from degree-based hiring and toward competency-based talent assessments.

This trend follows research by Google, which found that GPA scores are generally poor indicators for candidate performance.

An online application form that includes some basic technical questions can help assess the competency level of candidates much more accurately, with little to no overhead.

3. A structured technical interviewTo ensure that each candidate receives an equal opportunity during the interview process, and to eliminate hidden or implicit biases, we must standardize as much of the process as possible.

Source: UnsplashA structured interview process generally consists of 3 parts:

  1. All interviews include the same set of questions.
  2. The questions are asked in the same order.
  3. The interviewers agree on the range of acceptable answers beforehand.

This method enables companies to run multiple interviews concurrently, using multiple interviewers, that deliver comparable results that can be measured.

It also ensures that candidates undergo a similar interview process regardless of the interviewer.

It’s important to note that agreeing on which answers are correct/incorrect does not suggest that candidates who fail to provide a correct answer fail the interview – it simply ensures that there’s a common understanding of the level of answer they provided.

Starting to follow a structured interview process can be a jarring experience that requires significant upfront investment and may encounter some resistance from interviewers who enjoyed relative freedom in their interview process.

To help adopt the process I suggest reading the material provided by the U.S. Office of Personnel Management and Comeet.

Source: UnsplashImplicit biases are broad sets of assumptions we make about individuals or groups, which are hard to eliminate.

They are especially harmful during job interviews, where an interviewer can unintentionally give unfair advantages or disadvantages to candidates.

Two practical ways to reduce their effect on the interview process are:

  1. Interviewing in pairs is a great way to improve the interview process, as it enables the interviewers to help, provide feedback, and reduce the chance that an implicit bias on the part of one of the interviews affects the outcome of the process.
  2. Ensure that the interview process involves at least two separate teams and hold the feedback until the end. This way, we avoid contaminating the second interview and enable a productive discussion between both teams.

These suggestions are part of Microsoft’s Alternative interview framework that has shown great results.

You can find additional material on implicit biases at the National Institute of Health.

4. Technical questionsSource: UnsplashSolving algorithm puzzles on a whiteboard or through Leetcode and similar sites provides little insight into candidates’ ability to perform their roles.

In 2015, Max Howell, the developer of Homebrew, popular package manager for macOS, failed a job interview at Google.

After which, he published the following tweet:

Google: 90% of our engineers use the software you wrote (Homebrew), but you can’t invert a binary tree on a whiteboard so fuck off.

— Max Howell (@mxcl) June 10, 2015

It’s unlikely that Howell’s ability to invert a binary tree reflects on how well he can architect a package management tool, especially with his track record.

Instead, we can devise a technical test focusing on specific skills within the roles domain that allow the candidate to demonstrate how well they can perform in relevant tasks.

Two examples I’ve encountered in the past that provided consistently good results are:

  1. Extract a small part of your code base (a small microservice or a set of U.I. components). Ensure that it has good unit test coverage before removing some functionality and breaking the tests. The candidate is asked to fix the code so that it passes all the unit tests successfully. Once the candidate finishes the task, go over their solution with them and discuss how they chose to tackle the problem, which areas were harder for them, and how they would have improved the code base if they had had more time.
  2. Alternatively, you can provide the candidate with a home assignment with a reasonable time-boxed problem that enables the candidate to avoid much of the stress involved in live coding. Once the task is complete, invite the candidate to an interview where you go over the solution with them. Try to focus more on the hows and whys than on which parts they finished correctly. The goal is to understand the candidate’s thinking process and how they approach relevant problems.

While there’s no magic formula for the ‘perfect interview,’ having a clear role definition, focusing on the correct qualifications, following a structured interview process, and matching your technical questions to the role will provide the optimal setting for success.

Did the advice in the post help with your interview process? Do you have other suggestions to share with our readers? Let us know, and we will incorporate the best ones in future follow-ups to this article.

The post 4 Ways to Improve Your Technical Interview Process appeared first on Semaphore.

View Details

The availability and quality of documentation are perhaps the most valuable assets for helping developers do their job. Likewise, good documentation will make the difference for users in choosing one product instead of another. However, finding the right tooling for managing and hosting documentation it’s a whole task on its own. It is even harder for open-source projects, whose developers might not have the resources for setting up and maintaining a documentation website that can live up to their product. In this last regard, Docusaurus is an open-source project that aims to make it easy for developers to build, deploy, and maintain documentation websites. In this article, we’ll take a closer look at Docusaurus’ features and how it tackles the main concerns of complex documentation projects.

Edited transcriptionDocusaurus is a static site generator designed to quickly build and get online documentation and blog websites. “We market Docusaurus as a content-centric website generator, but it’s also very flexible and you can create any kind of website,” says Sébastien Lorber, Docusaurus’ lead developer and maintainer.

As Sébastien explains, Docusaurus emerged from the necessity of a method for documenting open-source projects with higher levels of customization. Creating a custom documentation site requires investing time and money, an investment that grows exponentially when working with different documentation projects. The industry’s cheap and straightforward alternative is using Jekyll templates. Yet, this approach has flaws of its own. ”Every time you make an update to add new features to the template, you have to backport these new updates to all the documentation projects one by one,” Sébastien says.

How Docusaurus worksJust like Jekyll, Docusaurus is a single-page static site generator. Single-page websites show content on a single page instead of being broken up into different ones like most websites. Single-page websites are simpler to browse and reduce load times from the user end.

The idea of Docusaurus was to create a tool where you can just write Markdown files and then have some configuration,” says Sébastien. Docusaurus simplifies the website configuration process by making it as simple as writing a Markdown file as you would document a GitHub repository. On the server side, Docusaurus uses React, employing an extended version of markdown known as MDX, which integrates JavaScript’s syntax extension JSX. In this way, Docusaurus is able to render Markdown files as React components and provide extra capabilities like embedded components and support for testing.

What’s more, Docusaurus can be deployed in a glimpse. For hosting a Docusaurus static website,Sébastien recommends using Jamstack providers such as Vercel or Netlify. These services offer a deployed preview in a way that “if you edit docs in your pull request, you can have a deployment preview being posted in the pull request with a link where the reviewer can just click on the link and see the updated doc live.” Docusaurus makes validating changes as straightforward as taking a look at the preview without having to check it out locally.

Following, we will mention the main features of Docusaurus that Sébastien shared with us.

Content searchSearch and indexation is one of the main assets users have when using a knowledge base. For helping users find the exact piece of information they are looking for, Docosaurus uses DocSeach, a tool from the product search and discovery engine software Algolia.

This search plugin, explains Sébastien, “crawls the HTML files that we generate and creates some kind of JSON file with all the search tokens that the search engine will use.” Additionally, Docusaurus has integration to add required UI elements such as search boxes and search results display. Moreover, community plugins are also available for local searches.

User authenticationUser authentication is necessary to show documentation to certain users exclusively, as in the case of a team developing a product privately. Previously-mentioned hosts for Docusaurus like Netlify and Vercel offer options such as defining a user and password credentials for accessing the static files.

Another alternative is using edge functions. An edge function is an endpoint or application that runs on the edge of a network. Edge functions are often used to perform tasks in the server closer to the user that would normally be performed by a centralized server. This includes tasks like processing data, providing cloud services, and, in this case, authenticating the user. Edge functions can be run before serving the static files on the host, in a way that allows, as Sébastien puts it, “to add an authentication workflow on top of a static site.” The edge function can check if a user has the right cookie and decide if redirects him to a given logging page or static website or not.

Translation-readyDocumentation needs to be available in the language the user decides to use the product. Docusaurus supports localization through an internationalization file system convention known as i18n. “If you have documentation, you can just copy this documentation to a new folder and then you will be able to add the new locale that you want to have on your site to the config, and then you run the Docusaurus key, and you will build your site for the two locals,” explains Sébastien.

In Docusaurus, each locale is sandboxed in a separate single-page application located in a specific folder. “You will build, for example, a single-page application for the English locale, and then you will build one for the French locale,” says Sébastien.

Documentation versioningWhen different versions of the same software product are available, documentation has to split up. While new versions need to be documented, legacy versions’ documentation has to remain available for those still using them. Historically, points out Sébastien, the most popular approach for this task is a “versioning strategy based on Git branches”: the main branch has the documentation of the currently developed version, while an archived branch holds the documentation of previous versions. However, Sébastien understands that “the problem with this model is that, sometimes, you have a lot of small versions that are quite consecutive, and when you do some edits in one branch, then you have to backport the changes to the previous versions because it’s still relevant.”

In turn, to backport changes, Docusaurus has the documentation of all the versions in the main branch by default. Instead of branches, Docusaurus uses a folder system to version the documentation. In this way, “if you have to edit some docs, instead of backporting the change to all the older versions, you can directly edit all the versions that went in the same branch and submit in the single pull request with all the changes,” says Sébastien. Lastly, in case of very old versions that won’t be updated nor have to be backported, these can be archived and uploaded to a standalone branch.

The bottom lineSébastien is currently working on Docusaurus’ 3.0 version. By upgrading the support for React components into Markdown files, this future version will focus on improving documentation interactivity even further.

Follow Sébastien on Twitter. Sébastien also writes a React newsletter with more than 10,000 subscribers known as This Week in React in French and English.

To learn more about Docusaurus, visit docusaurus.io or try a live demo.

The post Sébastien Lorber on Building Documentation Websites Quickly and Easily With Docusaurus appeared first on Semaphore.

View Details

This tutorial will be your gentle guide to property-based testing. Property-based testing is a testing philosophy; a way of approaching testing, much like unit testing is a testing philosophy in which we write tests that verify individual components of your code.

By going through this tutorial, you will:

  • learn what property-based testing is;
  • understand the key benefits of using property-based testing;
  • see how to create property-based tests with Hypothesis;
  • attempt a small challenge to understand how to write good property-based tests; and
  • Explore several situations in which you can use property-based testing with zero overhead.

What is Property-Based Testing?In the most common types of testing, you write a test by running your code and then checking if the result you got matches the reference result you expected. This is in contrast with property-based testing, where you write tests that check that the results satisfy certain properties. This shift in perspective makes property-based testing (with Hypothesis) a great tool for a variety of scenarios, like fuzzing or testing roundtripping.

In this tutorial, we will be learning about the concepts behind property-based testing, and then we will put those concepts to practice. In order to do that, we will use three tools: Python, pytest, and Hypothesis.

  • Python will be the programming language in which we will write both our functions that need testing and our tests.
  • pytest will be the testing framework.
  • Hypothesis will be the framework that will enable property-based testing.

Both Python and pytest are simple enough that, even if you are not a Python programmer or a pytest user, you should be able to follow along and get benefits from learning about property-based testing.

Setting up your environment to follow alongIf you want to follow along with this tutorial and run the snippets of code and the tests yourself – which is highly recommendable – here is how you set up your environment.

Installing Python and pipStart by making sure you have a recent version of Python installed. Head to the Python downloads page and grab the most recent version for yourself. Then, make sure your Python installation also has pip installed. [pip] is the package installer for Python and you can check if you have it on your machine by running the following command:

python -m pip --version (This assumes python is the command to run Python on your machine.) If pip is not installed, follow their installation instructions.

Installing pytest and Hypothesispytest, the Python testing framework, and Hypothesis, the property-based testing framework, are easy to install after you have pip. All you have to do is run this command:

python -m pip install pytest hypothesis --upgradee This tells pip to install pytest and Hypothesis and additionally it tells pip to update to newer versions if any of the packages are already installed.

To make sure pytest has been properly installed, you can run the following command:

```

python -m pytest --versionpytest 7.2.0 ``` The output on your machine may show a different version, depending on the exact version of pytest you have installed.

To ensure Hypothesis has been installed correctly, you have to open your Python REPL by running the following:

python and then, within the REPL, type import hypothesis. If Hypothesis was properly installed, it should look like nothing happened. Immediately after, you can check for the version you have installed with hypothesis.__version__. Thus, your REPL session would look something like this:

```

import hypothesis>>> hypothesis.version'6.60.0' ``` Your first property-based testIn this section, we will write our very first property-based test for a small function. This will show how to write basic tests with Hypothesis.

The function to testSuppose we implemented a function gcd(n, m) that computes the greatest common divisor of two integers. (The greatest common divisor of n and m is the largest integer d that divides evenly into n and m.) What’s more, suppose that our implementation handles positive and negative integers. Here is what this implementation could look like:

def gcd(n, m): """Compute the GCD of two integers by Euclid's algorithm.""" n, m = abs(n), abs(m) n, m = min(n, m), max(n, m) # Sort their absolute values. while m % n: # While `n` doesn't divide into `m`: n, m = m % n, n # update the values of `n` amd `m`. return n If you save that into a file, say gcd.py, and then run it with:

```

python -i gcd.py ``` you will enter an interactive REPL with your function already defined. This allows you to play with it a bit:

λ python -i main.py>>> gcd(15, 6)3>>> gcd(15, 5)5>>> gcd(-9, 15)3 Now that the function is running and looks about right, we will test it with Hypothesis.

The property testA property-based test isn’t wildly different from a standard (pytest) test, but there are some key differences. For example, instead of writing inputs to the function gcd, we let Hypothesis generate arbitrary inputs. Then, instead of hardcoding the expected outputs, we write assertions that ensure that the solution satisfies the properties that it should satisfy.

Thus, to write a property-based test, you need to determine the properties that your answer should satisfy.

Thankfully for us, we already know the properties that the result of gcd must satisfy:

“[…] the greatest common divisor (GCD) of two or more integers […] is the largest positive integer that divides each of the integers.”1

So, from that Wikipedia quote, we know that if d is the result of gcd(n, m), then:

  1. d is positive;
  2. d divides n;
  3. d divides m; and
  4. no other number larger than d divides both n and m.

To turn these properties into a test, we start by writing the signature of a test_ function that accepts the same inputs as the function gcd:

def test_gcd(n, m): ... (The prefix test_ is not significant for Hypothesis. We are using Hypothesis with pytest and pytest looks for functions that start with test_, so that is why our function is called test_gcd.)

The arguments n and m, which are also the arguments of gcd, will be filled in by Hypothesis. For now, we will just assume that they are available.

If n and m are arguments that are available and for which we want to test the function gcd, we have to start by calling gcd with n and m and then saving the result. It is after calling gcd with the supplied arguments and getting the answer that we get to test the answer against the four properties listed above.

Taking the four properties into account, our test function could look like this:

def test_gcd(n, m): d = gcd(n, m) assert d > 0 # 1) `d` is positive assert n % d == 0 # 2) `d` divides `n` assert m % d == 0 # 3) `d` divides `m` # 4) no other number larger than `d` divides both `n` and `m` for i in range(d + 1, min(n, m)): assert (n % i) or (m % i) Go ahead and put this test function next to the function gcd in the file gcd.py. Typically, tests live in a different file from the code being tested but this is such a small example that we can have everything in the same file.

Plugging in HypothesisWe have written the test function but we still haven’t used Hypothesis to power the test. Let’s go ahead and use Hypothesis’ magic to generate a bunch of arguments n and m for our function gcd. In order to do that, we need to figure out what are all the legal inputs that our function gcd should handle.

For our function gcd, the valid inputs are all integers, so we need to tell Hypothesis to generate integers and feed them into test_gcd. To do that, we need to import a couple of things:

from hypothesis import given, strategies as st given is what we will use to tell Hypothesis that a test function needs to be given data. The submodule strategies is the module that contains lots of tools that know how to generate data.

With these two imports, we can annotate our test:

from hypothesis import given, strategies as stdef gcd(n, m): ...@given(st.integers(), st.integers())def test_gcd(n, m): d = gcd(n, m) # ... You can read the decorator @given(st.integers(), st.integers()) as “the test function needs to be given one integer, and then another integer”. To run the test, you can just use pytest:

λ pytest gcd.py (Note: depending on your operating system and the way you have things configured, pytest may not end up in your path, and the command pytest gcd.py may not work. If that is the case for you, you can use the command python -m pytest gcd.py instead.)

As soon as you do so, Hypothesis will scream an error message at you, saying that you got a ZeroDivisionError. Let us try to understand what Hypothesis is telling us by looking at the bottom of the output of running the tests:

...gcd.py:8: ZeroDivisionError--------------------------------- Hypothesis ----------------------------------Falsifying example: test_gcd( m=0, n=0,)=========================== short test summary info ===========================FAILED gcd.py::test_gcd - ZeroDivisionError: integer division or modulo by zero============================== 1 failed in 0.67s ============================== This shows that the tests failed with a ZeroDivisionError, and the line that reads “Falsifying example: …” contains information about the test case that blew our test up. In our case, this was n = 0 and m = 0. So, Hypothesis is telling us that when the arguments are both zero, our function fails because it raises a ZeroDivisionError.

The problem lies in the usage of the modulo operator %, which does not accept a right argument of zero. The right argument of % is zero if n is zero, in which case the result should be m. Adding an if statement is a possible fix for this:

def gcd(n, m): """Compute the GCD of two integers by Euclid's algorithm.""" n, m = abs(n), abs(m) n, m = min(n, m), max(n, m) # Sort their absolute values. if not n: return m while m % n: # While `n` doesn't divide into `m`: n, m = m % n, n # update the values of `n` and `m`. return n However, Hypothesis still won’t be happy. If you run your test again, with pytest gcd.py, you get this output:

```

pytest gcd.py...FAILED gcd.py::test_gcd - assert 0 > 0 ``` This time, the issue is with the very first property that should be satisfied. We can know this because Hypothesis tells us which assertion failed while also telling us which arguments led to that failure. In fact, if we look further up the output, this is what we see:

n = 0, m = 0 <-- Hypothesis tells you what arguments broke the test @example(0, 0) @given(st.integers().filter(lambda n: n != 0), st.integers()) def test_gcd(n, m): d = gcd(n, m)> assert d > 0 # 1) `d` is positiveE assert 0 > 0E Falsifying explicit example: test_gcd(E n=0, m=0,E )gcd.py:23: AssertionError===================== short test summary info =====================FAILED gcd.py::test_gcd - assert 0 > 0 This time, the issue isn’t really our fault. The greatest common divisor is not defined when both arguments are zero, so it is ok for our function to not know how to handle this case. Thankfully, Hypothesis lets us customise the strategies used to generate arguments. In particular, we can say that we only want to generate integers between a minimum and a maximum value.

The code below changes the test so that it only runs with integers between 1 and 100 for the first argument (n) and between -500 and 500 for the second argument (m):

@given( st.integers(min_value=1, max_value=100), st.integers(min_value=-500, max_value=500),)def test_gcd(n, m): d = gcd(n, m) # ... That is it! This was your very first property-based test.

Why bother with Property-Based Testing?To write good property-based tests you need to analyse your problem carefully to be able to write down all the properties that are relevant. This may look quite cumbersome. However, using a tool like Hypothesis has very practical benefits:

  • Hypothesis can generate dozens or hundreds of tests for you, while you would typically only write a couple of them;
  • tests you write by hand will typically only cover the edge cases you have already thought of, whereas Hypothesis will not have that bias; and
  • thinking about your solution to figure out its properties can give you deeper insights into the problem, leading to even better solutions.

These are just some of the advantages of using property-based testing.

Using Hypothesis for freeThere are some scenarios in which you can use property-based testing essentially for free (that is, without needing to spend your precious brain power), because you don’t even need to think about properties. Let’s look at two such scenarios.

Testing RoundtrippingHypothesis is a great tool to test roundtripping. For example, the built-in functions int and str in Python should roundtrip. That is, if x is an integer, then int(str(x)) should still be x. In other words, converting x to a string and then to an integer again should not change its value.

We can write a simple property-based test for this, leveraging the fact that Hypothesis generates dozens of tests for us. Save this in a Python file:

from hypothesis import given, strategies as st@given(st.integers())def test_int_str_roundtripping(x): assert x == int(str(x)) Now, run this file with pytest. Your test should pass!

FuzzingDid you notice that, in our gcd example above, the very first time we ran Hypothesis we got a ZeroDivisionError? The test failed, not because of an assert, but simply because our function crashed.

Hypothesis can be used for tests like this. You do not need to write a single property because you are just using Hypothesis to see if your function can deal with different inputs. Of course, even a buggy function can pass a fuzzing test like this, but this helps catch some types of bugs in your code.

Comparing against a gold standardSometimes, you want to test a function f that computes something that could be computed by some other function f_alternative. You know this other function is correct (that is why you call it a “gold standard”), but you cannot use it in production because it is very slow, or it consumes a lot of resources, or for some other combination of reasons.

Provided it is ok to use the function f_alternative in a testing environment, a suitable test would be something like the following:

@given(...)def test_f(...): assert f(...) == f_alternative(...) When possible, this type of test is very powerful because it directly tests if your solution is correct for a series of different arguments.

For example, if you refactored an old piece of code, perhaps to simplify its logic or to make it more performant, Hypothesis will give you confidence that your new function will work as it should.

The importance of property completenessIn this section you will learn about the importance of being thorough when listing the properties that are relevant. To illustrate the point, we will reason about property-based tests for a function called my_sort, which is your implementation of a sorting function that accepts lists of integers.

The results are sortedWhen thinking about the properties that the result of my_sort satisfies, you come up with the obvious thing: the result of my_sort must be sorted.

So, you set out to assert this property is satisfied:

@given(...)def test_my_sort(int_list): result = my_sort(int_list) for a, b in zip(result, result[1:]): assert a <= b Now, the only thing missing is the appropriate strategy to generate lists of integers. Thankfully, Hypothesis knows a strategy to generate lists, which is called lists. All you need to do is give it a strategy that generates the elements of the list.

from hypothesis import given, strategies as st@given(st.lists(st.integers()))def test_my_sort(int_list): result = my_sort(int_list) for a, b in zip(result, result[1:]): assert a <= b

Now that the test has been written, here is a challenge. Copy this code into a file called my_sort.py. Between the import and the test, define a function my_sort that is wrong (that is, write a function that does not sort lists of integers) and yet passes the test if you run it with pytest my_sort.py. (Keep reading when you are ready for spoilers.)

Notice that the only property that we are testing is “all elements of the result are sorted”, so we can return whatever result we want, as long as it is sorted. Here is my fake implementation of my_sort:

def my_sort(int_list): return [] This passes our property test and yet is clearly wrong because we always return an empty list. So, are we missing a property? Perhaps.

The lengths are the sameWe can try to add another obvious property, which is that the input and the output should have the same length, obviously. This means that our test becomes:

@given(st.lists(st.integers()))def test_my_sort(int_list): result = my_sort(int_list) assert len(result) == len(int_list) for a, b in zip(result, result[1:]): assert a <= b Now that the test has been improved, here is a challenge. Write a new version of my_sort that passes this test and is still wrong. (Keep reading when you are ready for spoilers.)

Notice that we are only testing for the length of the result and whether or not its elements are sorted, but we don’t test which elements are contained in the result. Thus, this fake implementation of my_sort would work:

def my_sort(int_list): return list(range(len(int_list))) Use the right numbersTo fix this, we can add the obvious property that the result should only contain numbers from the original list. With sets, this is easy to test:

@given(st.lists(st.integers()))def test_my_sort(int_list): result = my_sort(int_list) assert len(result) == len(int_list) # Should have same length. assert set(result) <= set(int_list) # Should use numbers from input. for a, b in zip(result, result[1:]): # Result is actually sorted. assert a <= b Now that our test has been improved, I have yet another challenge. Can you write a fake version of my_sort that passes this test? (Keep reading when you are ready for spoilers).

Here is a fake version of my_sort that passes the test above:

def my_sort(int_list): if not int_list: return [] return len(int_list) * [int_list[0]] The issue here is that we were not precise enough with our new property. In fact, set(result) <= set(int_list) ensures that we only use numbers that were available in the original list, but it doesn’t ensure that we use all of them. What is more, we can’t fix it by simply replacing the <= with ==. Can you see why?I will give you a hint. If you just replace the <= with a ==, so that the test becomes:

@given(st.lists(st.integers()))def test_my_sort(int_list): result = my_sort(int_list) assert len(result) == len(int_list) # Should have same length. assert set(result) == set(int_list) # Same numbers as input. for a, b in zip(result, result[1:]): # Result is actually sorted. assert a <= b then you can write this passing version of my_sort that is still wrong:

def my_sort(int_list): if not int_list: return [] s = sorted(set(int_list)) return s + [s[-1]] * (len(int_list) - len(s)) This version is wrong because it reuses the largest element of the original list without respecting the number of times each integer should be used. For example, for the input list [1, 1, 2, 2, 3, 3] the result should be unchanged, whereas this version of my_sort returns [1, 2, 3, 3, 3, 3].

The final testA test that is correct and complete would have to take into account how many times each number appears in the original list, which is something the built-in set is not prepared to do. Instead, one could use the collections.Counter from the standard library:

@given(st.lists(st.integers()))def test_my_sort(int_list): result = my_sort(int_list) assert len(result) == len(int_list) # Should have same length. assert Counter(result) == Counter(int_list) # Should use numbers from input. for a, b in zip(result, result[1:]): # Result is actually sorted. assert a <= b So, at this point, your test function test_my_sort is complete. At this point, it is no longer possible to fool the test! That is, the only way the test will pass is if my_sort is a real sorting function.

Use properties and specific examplesThis section showed that the properties that you test should be well thought-through and you should strive to come up with a set of properties that are as specific as possible. When in doubt, it is better to have properties that may look redundant over having too few.

Another strategy that you can follow to help mitigate the danger of having come up with an insufficient set of properties is to mix property-based testing with other forms of testing, which is perfectly reasonable.

For example, on top of having the property-based test test_my_sort, you could add the following test:

def test_my_sort_specific_examples(): assert my_sort([]) == [] assert my_sort(list(range(10)[::-1])) == list(range(10)) assert my_sort([42, 73, 0, 16, 10]) == [0, 10, 16, 42, 73] ConclusionThis article covered two examples of functions to which we added property-based tests. We only covered the basics of using Hypothesis to run property-based tests but, more importantly, we covered the fundamental concepts that enable a developer to reason about and write complete property-based tests.

Property-based testing isn’t a one-size-fits-all solution that means you will never have to write any other type of test, but it does have characteristics that you should take advantage of whenever possible. In particular, we saw that property-based testing with Hypothesis was beneficial in that:

  • Hypothesis can generate dozens or hundreds of tests for you, while you would typically only write a couple of them;
  • tests you write by hand will typically only cover the edge cases you have already thought of, whereas Hypothesis will not have that bias; and
  • thinking about your solution to figure out its properties can give you deeper insights into the problem, leading to even better solutions.

This article also went over a couple of common gotchas when writing property-based tests and listed scenarios in which property-based testing can be used with no overhead.

If you are interested in learning more about Hypothesis and property-based testing, we recommend you take a look at the Hypothesis docs and, in particular, to the page “What you can generate and how”.

The post Getting Started With Property-Based Testing in Python With Hypothesis and Pytest appeared first on Semaphore.

View Details

The Blue Green Deployment strategy allows for the testing of new deployments with production data before making them available to users. This type of deployment also has the added benefit of having no downtime during the deployment process.

Using blue green deployments requires a series of manual tasks. Cloud providers and Kubernetes can aid in this process, but sometimes you do not have access to these tools.

We (my firm) recently found ourselves in such a situation. We had a Node.js application running behind Nginx. Nginx was set up as a reverse proxy. This means it was receiving all public requests and forwarding them to the running Node.js application. All this was running on a single Linux VPS.

We wanted to set up a blue green deployment for this application in a process requiring minimal human intervention. The obvious solution is using a CI/CD platform. But, first we’d need to create the deployment manually to understand how it could work.

What we wanted to achieve was the following:

  • A push to the main branch should trigger a deployment to the currently non-live blue green environment.
  • After that, we should be able to press a Promote to Live button to promote the non-live environment to the live environment.
  • Once an environment is live, we should be able to roll back to the previous live environment by pressing the Rollback button.

This is the result that we achieved:

In this article, we will look at how we created this custom CI/CD-driven blue green deployment pipeline in depth. We will also discuss how we broke down the problem and the challenges that we faced and solved along the way.

PrerequisitesWe will be referencing this express application on GitHub throughout this article. This repository contains a minimal Node.js application that accepts incoming HTTP requests. It also contains the deployment scripts and the Semaphore CI/CD configuration.

How do blue green deployments work?By now, we know what blue green deployments allow us to do. Next, let’s understand how blue green deployments work. This will allow us to break down the problem.

Let’s take an example. Our application is currently running in the green environment. It is currently on Version 1.1.2 and is accessible at myapp.com.

We need to update our application to Version 1.2.0. To do this, we first create or update the blue environment with the code for Version 1.2.0. Then, we use the load balancer to direct traffic on blue.myapp.com to the blue environment.

After this point, we can do any sort of testing that we want to do on blue.myapp.com, be it manual or automated. After we are sure that blue is working as expected, we configure the load balancer to direct traffic on myapp.com to the blue environment. We can also access the green environment from green.myapp.com in case we need it for some reason.

After this point, the new version of the application is up. We can safely shut down green. We also have the choice to keep green around for some time in case something goes wrong with blue. In such a situation, we can quickly switch back to green.

Manual blue green deploymentTo perform blue green deployment using the current setup on the server, we need to have the following configuration:

  1. We need to have two copies of the repository, one for blue and the other for green.
  2. We need to run the Node.js application for both blue and green. Both of these should listen to different HTTP ports.
  3. We need to configure Nginx to forward requests for blue.myapp.com and green.myapp.com to their respective HTTP ports.
  4. We need to configure Nginx to forward requests for myapp.com to whichever environment is currently considered live.

Let us assume that blue is currently live. Then, we need to perform the following steps during deployment to green:

  1. Pull new code for green on the server
  2. Start/Restart the Node.js application for green
  3. Change the Nginx configuration for myapp.com to forward requests to the HTTP port that green is using.
  4. Apply the new configurations for Nginx

There are enough steps involved to give rise to a human error once in a while. This is why we found so much utility in automating this process.

Blue green deployment automationBefore we could even think about setting up a CI/CD pipeline, we needed to figure out a way to automate the deployment process.

We took some time to break down the automated deployment process into smaller steps. We ended up with the following list of tasks to automate:

  • Get the code to deploy for the blue or green environment on the server.
  • Start and stop the application for an environment.
  • Set up the Nginx configuration for the environment, so that it is accessible from blue.myapp.com and green.myapp.com.
  • Set up the Nginx configuration for the live environment. We can reuse this for both live deployments and rollbacks.
  • Delete the Nginx configuration for an environment. This is useful when shutting down an environment.

Getting the code on the serverThe first strategy that we tried was to install git on the server. We planned to pull the code from GitHub during the deployment process.

This strategy required setting up deploy keys on GitHub and then configuring SSH keys on the server. We did implement this and it worked wonders. Yet, we decided to scrap it for the following reasons:

  • The ultimate goal was to run this on a CI/CD pipeline. These already set up deploy keys on GitHub to pull code. Setting up GitHub access on the server again would be kind of redundant.
  • Setting up GitHub access on the server adds one more step during server setup. If we ever needed to migrate servers, we would need to set up GitHub access all over again.
  • Managing the Deploy Key on the server is extra overhead and a bit of a security risk.

Our new strategy was to write a script that could checkout the target code on its own.

A local dev environment would already have GitHub’s SSH-based authentication set up.

CI/CD environments already provide a way to checkout code on the branch or tag that the pipeline is running for. For this reason, the script would also have the option to accept a path to a directory. If this option is set, the script would deploy the code in the directory without pulling any code from GitHub.

After we had the code that we want to deploy checked out, we would need to build the application. Finally, we would need to copy it to a specific directory on the server. We opted to use the $HOME/environment directory. This directory would have one directory for each environment. Thus, we would have the following directories:

/home/username/environment/blue/home/username/environment/green We decided to use the rsync utility to copy the code to the server. We experimented with scp first but rsync was much faster compared to scp.

You’ll find the completed script that pushes the code at deployment/push-code.sh.

Starting and stopping the application for a specific environmentStarting a Node.js application is pretty straightforward. However, the plan was to run multiple versions of the application. Thus, some special considerations needed to be taken when starting the application for an environment.

  • Each environment would need to listen to different ports for HTTP
  • Each environment would need to be able to be stopped and started easily

Using different ports for each environment is pretty easy. All that is required is to configure the HTTP port as an environment variable. This can be accessed in code using Node.js’s process.env.

Starting the application for an environment is also a breeze.

Stopping the application is a different story. We would need to keep track of the process ids for the application for each environment.

A process id is a number that operating systems use to uniquely identify each process running on the system. We can then tell the operating system to end a process by referring to it using the process id.

To stop an application, we would first need to store the process ids for every environment that we would start. We would also need to keep this list updated as different environments are started and stopped. This would require writing more code so we decided to use a readily-available solution instead.

PM2 proved to be the perfect solution to this problem. It is a process manager which can start and stop Node.js applications.

The main utility of PM2 for our use case is that it allows assigning names to Node.js applications. We later used these assigned names to stop applications.

If we were to name the Node.js applications after their environments (which is what we did), we could start an environment by using this command:

HTTP_PORT=$HTTP_PORT pm2 start index.js --name "$ENVIRONMENT_NAME" This would also allow us to stop an environment by using the following command:

pm2 stop $ENVIRONMENT_NAME PM2 also brings other advantages to the table. It supports the following useful flags when starting an application:

  • -i: The number of instances to create. If you use -i number, it will create that many instances. If you do not specify a number, it will default to the number of CPU cores available.
  • --wait-ready: This instructs PM2 to wait for a ready signal from the process.
  • --listen-timeout 10000: This instructs PM2 to wait for the ready signal for 10,000 milliseconds.
  • --kill-timeout 10000: We use this for graceful shutdowns. PM2 will wait for 10,000 milliseconds for the process to exit by itself after sending the SIGINT signal. After that, it will kill the process.

So, running the application with these options will give us an application with support for running many instances and graceful shutdowns:

HTTP_PORT=$HTTP_PORT pm2 start index.js --name "$ENVIRONMENT_NAME" -i --wait-ready --listen-timeout 10000 --kill-timeout 10000 We decided to use <environment>.myapp.com as the name for each environment.

Now, all we needed were two scripts — one for starting an environment and the other for stopping an environment.

You’ll find the completed script for starting an environment at deployment/remote/environments/start-environment.sh and for stopping an environment at deployment/remote/environments/stop-environment.sh.

Setting up the Nginx configuration for an environmentThe next problem on the list was creating Nginx configurations dynamically. The configuration would need to have at least the following information:

  • The domain associated with the environment
  • The HTTP ports to proxy the requests to

However, we did not want to generate the configuration completely via the code.

If we did this, then future changes to the Nginx config would require changes to the deployment script. This could introduce bugs. It would also be difficult to view the history of the Nginx configuration on git. We wanted the configuration to be easy to maintain.

In the end, we decided to take inspiration from Mustache, a template syntax.

This syntax allows replacing placeholders in a string, writing conditionals, loops, etc. But, it would be overkill to use the full Mustache syntax. We only need basic string replacement.

Instead, we decided to use the sed utility, which is used to find and replace text in files.

To do this, we created a text file with the required Nginx configuration while using DOMAIN and HTTP_PORT as placeholders.

server { listen 80; server_name {{DOMAIN}}; location / { proxy_pass http://127.0.0.1:{{HTTP_PORT}}; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; }} The next step was to update the start-environment.sh script. We just needed to add a few lines of code to generate the Nginx configuration.

The code copies the template file to /etc/nginx/sites-enabled/environment-name.domain.com. Then it replaces the values of DOMAIN and HTTP_PORT in the file using sed. We use <environment>.myapp.com as DOMAIN and the HTTP port of the PM2 application for that app as HTTP_PORT.

The last thing it does is create a symlink from /etc/nginx/sites-available/environment-name.domain.com to /etc/nginx/sites-enabled/environment-name.domain.com. Placing the configuration in the sites-enabled directory lets Nginx know that we want the configuration to be enabled.

Setting up the Nginx configuration for the live environmentIf any of the environments we created earlier is accessible from the main domain, myapp.com, it is the live environment.

We can either reuse the earlier configuration template or create a new one for the live environment.

For the live environment, we would need to copy the template to /etc/nginx/sites-available/myapp.com and create a symlink at /etc/nginx/sites-enabled/myapp.com.

For the value of DOMAIN, we use myapp.com. The HTTP_PORT is the port for the PM2 application for the same environment that we are promoting to the live environment.

The only problem here was finding the port where the target environment is running. We first scoured the PM2 documentation to find a command that would give us the ports used by an application. However, we did not find any such information. So, we decided to use another strategy.

We added the port information as a comment to the Nginx configuration itself!

```

environment:{{ENV_NAME}};{{HTTP_PORT}}

``` We also decided to include the name of the environment for good measure. So, the Nginx configuration now looked like this:

```

environment:{{ENV_NAME}};{{HTTP_PORT}}server { listen 80; server_name {{DOMAIN}}; location / { proxy_pass http://127.0.0.1:{{HTTP_PORT}}; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; }}

``` At this point, we can do the following to promote a running environment to live:

  • Find the Nginx configuration for the target environment in /etc/nginx/sites-available/<environment>.myapp.com.
  • Extract the first line from the configuration–this is the comment that we added. Extract HTTP_PORT from the line using a regular expression.
  • Create a live Nginx configuration using the extracted HTTP_PORT and ENV_NAME.

You’ll find the completed script at deployment/remote/environments/set-live-environment.sh.

Deleting the Nginx configuration for an environmentFor this, we would need to delete the configuration file for the target environment at /etc/nginx/sites-available. That’s it!

We updated the stop environment script with code to achieve this.

Test running the scriptsWe already had the scripts necessary to perform the deployment. However, we hit a couple of roadblocks when we tried to run the scripts over SSH.

The first problem was that our script was not allowed to write to Nginx’s sites-enabled and sites-available directories. We resolved this by changing the ownership of these directories to the Linux user that we were using to run the deployment scripts. We used the chown command to achieve this.

The second problem was that we were calling systemctl restart nginx after changing the Nginx configurations. However, this command was also only accessible to a super user. This was a bit more challenging. But we figured out that there is something called the sudoers file.

We just needed to add the following line to the /etc/sudoers file.

deployuser ALL=NOPASSWD: /bin/systemctl restart nginx Here, deployuser is the name of the Linux user we were using for running the deployment scripts.

The addition of the above line allows the user with the specified username to run sudo systemctl restart nginx without the need to enter a super user password. Perfect!

Tighter scriptsAt this point, we could already do blue green deployment, but we could also do all sorts of other cool things.

For example, we could deploy a specific branch to a subdomain to test it out on production data! This was powerful but opened doors to making mistakes during deployment. So, we decided to create more scripts that would allow us to work with only the blue and green environments.

So, we created the following scripts:

  • push-code-blue-green-non-live This script figures out what the current non-live environment is among blue and green. Then, it runs our push-code script with the correct environment variables.
  • start-blue-green-non-liveThis script figures out the non-live environment and then runs the start-environment script with the correct environment name and HTTP port. We also hard-coded the required target HTTP ports for blue and green in this script. We decided to go for 3001 for blue and 3000 for green.
  • stop-blue-green-non-live This script figures out the non-live environment and runs the stop-environment script with the correct environment name.
  • switch-blue-green This script figures out the non-live environment and runs the set-live-environment script with the correct environment name.

All these scripts except push-code-blue-green-non-live run on the server itself. push-code-blue-green-non-live runs in the CI/CD pipeline or locally.

The scripts that run on the server can figure out the current live environment by parsing the comment that we placed on top of the Nginx configurations.

However, push-code-blue-green-non-live needs a way to figure out the current non-live environment. So, we decided to add one final script that echoes the non-live environment’s name. You’ll find this script in get-non-live-environment. We run this script through SSH from within the push-code-blue-green-non-live script to get the current non-live environment.

In the end, we decided to also run this new script from the other blue green scripts. This allows us to have a single source of truth for the name of the non-live environment.

Integrating the CI/CD pipelineWe wanted to achieve the following CI/CD pipeline:

We decided to use Semaphore for this. We created three different Semaphore workflows and connected them all through Semaphore’s promotions feature. This allows Semaphore workflows to trigger and branch off into other workflows. We configured the entry workflow to run every time we push code to the main branch. The entry workflow is a yml file located in .semaphore/semaphore.yml at the base path of the repository.

This workflow performs the following steps:

  • Runs tests.
  • Uses scp to copy the deployment scripts from the repository to ~/deployment on the server.
  • Runs push-code-blue-green-non-live. This pushes code to the currently non-live environment.
  • Runs start-blue-green-non-live on the server over SSH. This starts the non-live environment to which we pushed the code.

This workflow also defines a promotion named Promote to Production. Defining this promotion displays a button on the Semaphore UI to manually trigger it. This is exactly what we wanted! This is another workflow defined in .semaphore/promote-to-production.yml.

This workflow runs the switch-blue-green script on the server over SSH. This is all that we need to promote the current non-live environment to production.

The Promote to Production workflow defines a promotion named Rollback. This is a workflow defined in .semaphore/rollback-production.yml.

A rollback on a blue green deployment requires only switching the live environment. Thus, the Rollback workflow does the same thing as Promote to Production. In the end, our whole CI/CD Pipeline looks like this:

It would be fairly trivial to automate these promotions. However, blue green deployments are supposed to need user intervention. The great thing about using promotions for our use case is that it displays buttons which we can use to drive the deployment process. This eliminates the need to manually run scripts!

Final thoughtsThis blue green deployment mechanism completely replaced our manual deployment process. The deployment process does not require any mental overhead because it is automated. As a result, our confidence in our deployments has gone up. We can always rollback our deployments with the click of a button. This means that we also deploy more frequently. Another major win we have achieved is the ability to run tests and benchmarks on production data.

During the implementation phase, we also learnt more about CI/CD integration, Nginx, bash scripting, and the Linux permissions system. It was also a lot of fun to solve the challenges that we encountered along the way.

The post Blue Green Deployment for Node.js Without Kubernetes appeared first on Semaphore.

View Details

Statically typed code, especially when introduced incrementally to a codebase, can improve reliability and developer productivity while maintaining readability. Stripe calls Sorbet “doubling down on what makes Ruby delightful”. In this article we’ll explore what static typing is, how to add Sorbet to a Ruby on Rails application, and even how to run Sorbet in a continuous integration pipeline.

Dynamic Typing vs Static TypingProgramming languages can generally fall into one of 2 categories — Statically-Typed and Dynamically-Typed.

Ruby is a dynamically-typed language. This means that the Ruby interpreter does its best to infer the type of objects, on the fly, at runtime.

Statically-typed languages, such as Java, require that the programmer declares the type of an object when they declare the object.

Most Ruby programmers love dynamic typing. It leads to shorter, cleaner code that looks more like written English. It enables programmers to decouple types in method definition, leading to less complex diffs when changing the structure of a program. Still, dynamic typing presents some challenges.

The flexibility that comes with dynamic typing can sometimes come around to bite the programmer, or more likely, the user. Not having compile-time type-checking requires a programmer to know what type the objects they are working with have, or more importantly, what methods are available on that object. Type errors might only present in a subset of cases, making them particularly dangerous and tricky to track down.

How adopting static type checking affects site reliabilityA lot of ruby programmers might kill me for saying this, but explicit types are good. Linus Torvalds was right when he called us “strange people”.

Static types reduce errors by catching misused types before runtime, which directly impacts users in a positive way. Trading some of what makes Ruby programmer-friendly in order to be user-friendly is a fair trade.

Stripe actually claims that the primary goal of Sorbet is to make programmers happier and more productive, and that increased reliability is a secondary benefit. With static types, you can define NameError: uninitialized constant errors almost completely out of existence.

Type checking doesn’t eliminate all errors. In fact it might actually introduce more errors in development as you add type annotations and discover objects being misused. It’s always better, however, that a programmer discovers these sorts of errors, rather than a user.

When production incidents do happen, type annotations may make the errors even more helpful. Having some output that describes why code went wrong is undeniably better than NameError: uninitialized constant.

How to add Sorbet to a Rails appAdding Sorbet to an existing Ruby on Rails application can be done incrementally.

Sorbet actually comes as three gems, which you’ll need to add to your Gemfile:

gem 'sorbet', :group => :developmentgem 'sorbet-runtime'gem 'tapioca', require: false, :group => :development The Sorbet gem runs as an executable from the command line. The sorbet-runtime gem provides for runtime syntax. And tapioca is a tool for generating .rbi files. After adding these to your Gemfile, install them by running:

bundle install After installation, initialize sorbet with tapioca by running:

bundle exec tapioca init At this point, you may see a fair deal of errors if you’re in a rails project. The rails framework uses quite a fair deal of metaprogramming, meaning a lot of methods don’t technically exist until runtime. If methods don’t exist, they’re kind of hard to type check.

Non-rails Ruby projects won’t quite have as much of a problem here, but it is a challenge when adding sorbet to a rails project. Fortunately, the tapioca gem makes some of this easier. With a series of commands, we can generate the necessary .rbi files for sorbet to run type checking for the first time. To do this, run the following in your project’s directory:

bin/tapioca gems Then run:

bin/tapioca requir

Then run:

bin/tapioca dsl This should be enough for you to run srb tc without errors for the first time.

If you are still getting some errors, but you want to move on, you can type check a single file by going to the directory running srb tc <path-to-file> from outside the project. Note that this will still prove troublesome for Rails, applications as Sorbet will not be aware of types defined outside of the given file.

By default, Sorbet actually suppresses type checking warnings. To enable them, you’ll need to add a comment to the top of a given Ruby file.

Adding # typed: true to the top of a file will cause Sorbet to report errors on things like non-existent methods and incorrect argument counts.

Adding # typed: strict will require that all methods have method signatures and that all instance variables have explicit types.

You can read more about enabling static type checking in Stripe’s docs here as well as insights into writing method signatures.

Running Sorbet in CIOne great benefit of running type checking from the command line is it allows easy integration into your existing continuous integration system. This can ensure that existing standards are enforced before code is merged into the main branch.

If you don’t already have an existing continuous integration pipeline, you can get started quickly with SemaphoreCI.

First, sign up for semaphore – it’s best to sign up through Github to connect to your project easier. Click “Create New”, and select “Choose Repository” to connect Semaphore to an existing Github repository.

Select the basic “Ruby on Rails” workflow and edit it to add a line for bundle exec srb tc:

checkoutsem-version ruby 3.1.0cache restorebundle install --deployment --path vendor/bundlecache storebundle exec srb tc ConclusionMicrosoft already evolved JavaScript with TypeScript, which is increasing in popularity. Dynamic typing is one of the things that makes Ruby such a joy to program with, so to some it may seem counterintuitive to introduce types. Still, introducing types into Ruby code leads to safer code and arguably more productive programmers.

Sorbet allows us to gradually introduce static typing into an existing Ruby codebase. Sorbet suppresses type warnings by default, so adding it to a new Ruby codebase is not overwhelming. Adding type checking file-by-file with increasing degrees of strictness presents a good opportunity for incremental adoption.

Using Sorbet in a Rails codebase presents a set of challenges that we can work through, usingTapioca to empower us to begin type checking in a way that will lead to decreased (but more helpful) errors and (ideally) a better developer experience. Rails relies on a great deal of metaprogramming, which is challenging to type-check. When adding Sorbet to a Ruby on Rails project, you immediately face a number of errors that would take quite a bit of time to resolve manually. Fortunately, Tapioca makes this easier by generating interface files that allow Sorbet to run without errors for the first time.

The post How to Use Sorbet to Type-Check Ruby appeared first on Semaphore.

View Details

Our goal at Semaphore is to provide you with the fastest CI/CD platform possible. A big part of that is the most powerful hardware. That is why we’re adding new machines with better CPUs, more disk, and memory that can speed up your workflows by up to 50%.

Do more with these machinesThe new machines come in two generations:

  • E2 series: a direct upgrade from the classic E1 series. This will eventually become the new default. A balance of computing power, memory, and cost. It is a good choice for many applications.
  • F1 series: a new option for those running CPU-intensive jobs. F1 machines deliver the highest performance.

Here are the key differences between these two new machine types and the classic E1 generation:

  • Latest generation of processors – E2 and F1 generations are powered by 3.6 GHz AMD Ryzen 5 and 4.6 GHz Intel i5 processors respectively, resulting in a noticeable difference in pipeline performance and speed.
  • We are doubling the RAM – This means that you can now use machines with lower CPU counts for those memory-intensive apps and jobs.
  • Up to three times free disk space – Doubling the size of the disk means that there is almost three times free disk space left for your jobs that require large storage.

Already achieving amazing resultsWe started evaluating the new machines early in 2022. Throughout the year we ran extensive benchmarks and fine-tuned them to get the best performance. In the last quarter, customer trials began.

The new generation of machines was much welcomed since they improved build times and saved costs. Here are success stories from two of our customers.

Success story 1: cutting build times in halfWe´ve written in the past why continuous integration should take less than 10 minutes to be effective. The problem with slow builds is that they lead to a long feedback cycle where developers lose momentum, which hurts productivity and increases frustration.

The organization in this success story was suffering from low productivity due to its 20-minute pipeline. After initial troubleshooting, they found the cause for slowness: a large amount of single-thread jobs which didn’t benefit from having multiple-CPU machines.

They needed higher-clock CPUs and they found their solution by switching from e1-standard-2 to f1-standard-2. This change alone cut down build times by half.

The average build time dropped from 19 minutes to just over 11 minutes!

Success story 2: cutting the costs in halfFor another organization the value of the new machines came from the increased memory and disk size. They were already satisfied with the performance of their pipelines, however, their app required large amounts of memory and disk space to run so their only viable option was to use e1-standard-8 machines.

They decided to utilize the double memory and disk size that the E2 generation offers and have switched to e2-standard-4 machines.

Switching to a machine with a lower CPU count but larger memory/disk has cut their costs in half.

There was an added benefit too. Since E2 machines perform up to 20% better than E1s, the new setup performed slightly better (even with reduced CPU count) resulting in a bit faster pipelines that further reduced spending.

Frequently asked questionsI want to try the new machines. Help!Both the E2 and F1 generations of machines will be available by default on the upcoming Semaphore Startup and Scaleup plans. We will be launching the new plans in early Q1 2023.

If you are interested in testing out the machines sooner, please join our waiting list by filling out this form, and our team will contact you as soon as the spot opens.

What will happen with E1s?Nothing. Even though we plan to make E2 the new default in the future, the E1s will remain available.

What Operating Systems are available?Only the latest Ubuntu 20.04 image will be available on the new machines. We do not have plans to support the old Ubuntu 18.04 image on E2 and F1 machines.

Where can I find more info?Our docs have been updated, so make sure you check out our Machine Types docs page. If you want more information, reach out to our support team and we will make sure to expand our documentation.

Who doesn’t need more power?Be it because you need more memory or disk or simply because you want your CI to run faster, now you have more cost-effective options to run your workflows.

Thanks for reading, and happy building!

The post Power-Up Your Pipelines: New Machines Available appeared first on Semaphore.

View Details

The complexity of business and business processes can pull us away from the big picture. Internal documents, graphs, and other visual representations are there to aid us, but they might not be detailed enough, intuitive, or easy to read. In this regard, Wardley maps are business management tools that help organizations visualize their processes by plotting all their components topologically. In this episode, we will learn how to use maps to change a business position or strategy, represent possible scenarios or options, and improve decision-making.

Edited transcriptionSimon Wardley is confident of the results his invention, Wardley mapping provides. In 2008, when working at Canonical as manager of software services, he used mapping to define their business strategy and helped Ubuntu move from 3% of the OS market to 70% of all cloud computing systems. Maps, says Wardley, help them learn where to move forward, and what new, unidentified practices they could be part of.

Widely used in several industries and practices —from small businesses to government organizations—, Wardley maps came out as a response to uncertainty. As CEO of the photo services company Fotango in the early 2000s, Simon had a hard time grasping the extent of the services his company provided. Puzzled yet looking for help, he ran into Sun Tzu’s all-time classic The art of War. What stood out the most for Simon was Sun Tzu’s five factors in laying plans:

  1. Purpose. Having a purpose or moral imperative to what you are trying to do and why. This will determine why others would follow you.
  2. Earth. Describe and understand the landscape you’re competing in, including the position of troops, obstacles, and other topological features.
  3. Heavens. Understanding the climatic patterns of the landscape and how it affects the environment and the elements in it.
  4. Doctrine. Understanding doctrinal principles of organization, how you train your forces, and how you structure yourself and them around the landscape.
  5. Leadership. Choosing a strategy after reviewing the previous four factors.

Simon was surprised by how much importance Sun Tzu gave to the landscape in planning. Regardless of the differences between military and business strategy, Simon figured out it was crucial to think of his company as an environment to learn how it worked. In other words, he decided to make a map of it.

Simon asked throughout his company for every map there could be, including “mind maps, business process maps, and systems maps.” Still, it didn’t take long for Simon to realize that most of these were not maps, but graphs. While both maps and graphs connect concepts through nodes, in maps, space has meaning.

In maps, “where you put the nodes matters”, says Simon. “If you think about playing a game of chess, [….], the decision of moving a piece is determined by what we have learned about the environment, so when we move a piece, we’d learn about that move based upon the environment because we can observe the environment”, he adds. It is due to this additional layer of interaction, Simon believes, that “maps are very good for exploring territories and landscapes.”

After learning about this difference, Wardley devised a map for his organization. Maps, Simon argues, treat space in a way that provides intrinsic benefits for representation and observation that other forms of representation lack. Simon says that maps help to understand “the components involved in creating something, understanding how evolved those components are”, learn about the economic patterns in the industry, and use this information for anticipation. those for anticipation.”

To explain how mapping can provide valuable feedback, Simon gives the example of a mapping he did for an insurance company. His task was to take a look at a bottleneck in their business process: “they needed computers, they would order servers, servers would go into goods in, they’d modify, mount and rack it.” He found that they used custom racks for their customers instead of standard racks. Modifying racks added complexity, time, and cost to production. Yet, upon reflecting on it, the company couldn’t justify why they were using custom racks instead of standard. Perhaps it made sense someday, but not it didn’t anymore. So they optimized their process flow by ditching custom racks. According to Simon, mapping allowed the company to expose “their assumptions of the territory in a form that others can look at and question whether the map is right or wrong.”

By creating a representation of the business, its components, and how their work, maps show the company’s “assumptions over how something should be built, and that allows other people to see my assumptions and to challenge my assumptions through the map.” Maps also allow us to bypass conflict with business stories: “we have an entire industry tell everybody that great leaders are great storytellers, so if I have a story and you challenge my story, you’re saying you’re not a great leader, so I get very defensive.” Simon assures that challenging maps, in turn, don’t have this ego dilemma.

How to mapAccording to Simon, to create a map of something, you need to define three things:

  • Anchors. Anchors act as magnetic north to orient the map pieces by determining their position and setting where they can move to. “If I’m mapping out a competitive environment, I’ll use anchors of say the users and the business,” says Simon.
  • Value chain. The position of anchors within the map on the y-axis. While territorial maps have positions set in the cardinal positions, in mapping a business the key is mapping the supply chain and its need. As an example, mapping a tea shop could consist of mapping the public need for a cup of tea and the business need for selling them. It would also include the needs of a cup of tea, that is, the cup, tea, and hot water, but also the needs of the hot water and so on. Simon recommends using this axis as a representation of how visible components are for customers: from more to less visible.
  • Evolution. The x-axis marks the directions in which map elements can move while keeping consistency between them, or rather, how they evolve. To survive, says Simon, all businesses are subject to change and add more layers of complexity to their operations: “when we talk about evolution, we talk about the genesis of the novel and new, then those things evolve to become custom-built examples, then products and rental services and eventually commodity and utility services.”

Image source: Wardley Maps 1 — True user needs
Learning mappingSimon understands it takes three months to learn to map but reminds us that it demands practice, so it’s best to start early. Simon teaches mapping mostly to companies, groups from 20 to 30 people in a period of three to four weeks, doing a half a day session around each week. Simon separates teaching into four sessions or steps:

  1. Awareness. Get acquainted with most of the theory. Learning what mapping is, how it is possible to map space, and why situational awareness is important and useful.
  2. Practice. Pick a problem, and ask who are the users in that line of business and what are their needs. Later, ask what components are needed to satisfy those needs and how these have evolved. Simon shows them the mapping of a tea shop. Simon recommends the tea shop example since it’s something most people are familiar with. Then, he encourages you to do the same while looking at your organization and familiarizing yourself with its system.
  3. Patterns. Recognizing the “patterns that come out of mapping, particularly doctrine and organizational patterns”. To find these patterns, Simon recommends going back to the scenario in the previous step and analyzing it “from the viewpoint of principles and doctrine of how the organization runs.”
  4. Map refining. Polish your map by working “on anticipation, looking at competitors, their doctrine” and deciding where your company should step up to win the market. To encourage feedback on maps, he advises students to make different choices on the same scenario to refine their “understanding of what is possible in that space.”

Overall, Simon reminds us to always challenge our assumptions and allow others to do it as well when mapping: “it’s also a model, it’s going to be wrong. So that’s great, what you do is you share it with someone else and allow them to look at your assumptions and challenge what you’re doing”.

The bottom lineTo learn more about Wardley maps, Simon has a Wardley maps series on Medium. Additionally, you can check the community list of maps on Github where you will also find other useful resources such as videos, courses, and certifications in. If you want to keep updated with the latest from Simon, follow him on Twitter.

The post Simon Wardley on Improving Business With Maps appeared first on Semaphore.

View Details

When we combine the cloud with IaC tools like Terraform and continuous deployment we get the almost magical ability to create resources on demand. For all its benefits, however, the cloud has also introduced a set of difficulties, one of which is estimating cloud costs accurately.

Cloud providers have complex cost structures that are constantly changing. AWS, for example, offers 536 types of EC2 Linux machines. Many of them have similar names and features. Take for example “m6g.2xlarge” and “m6gd.2xlarge” — the only difference is that the second comes with an SSD drive, which will add $60 dollars to the bill. Often, making a mistake in defining your infrastructure can cause your bill to balloon at the end of the month.

It’s so easy to go above budget.​

We can set up billing alerts, but there are no guarantees that they will work. Alerts can happen during the weekend or be delayed, making us shoot past our budget in a few hours.

Another AWS billing horror story. This engineer did follow best practices in setting up billing alerts: still got no alerts due to delays in these alerts firing.

AWS has no ways to add hard spending limits – which Azure has – and these stories keep happening again and again. https://t.co/wFLifxqZco

— Gergely Orosz (@GergelyOrosz) June 30, 2022

So, how can we avoid this problem and use the cloud with confidence?

Enter InfracostInfracost is an open-source project that helps us understand how and where we’re spending our money. It gives a detailed breakdown of actual infrastructure costs and calculates how changes impact them. Basically, Infracost is a git diff for billing.

Infracost has two versions: a VSCode addon and a command line program. Both do the same thing: parse Terraform code, pull the current cost price points from a cloud pricing API, and output an estimate.

You can use Infracost pricing API for free or host your own. The paid tier includes a cloud dashboard to track changes over time.​

We can see the estimates right in the IDE:

Real-time cost estimation on VSCode.​

Or as comments in pull requests or commits:

Cost change information in the PR.​

Infracost also has an optional Infracost Cloud, which comes with a paid tier and includes features like Jira integration, custom price books, and a dashboard to keep track of costs over time.

The paid tier includes a dashboard to track spending over time.Setting up InfracostTo try out Infracost, we’ll need the following:

  • An Infracost API key. You can get one by signing up for free at Infracost.io.
  • The Infracost CLI installed in your machine.
  • Some Terraform files.

Once the CLI tool is installed, run infracost auth login to retrieve the API key. Now we’re ready to go.

The first command we’ll try is infracost breakdown. It analyzes Terraform plans and prints out a cost estimate. The --path variable must point to the folder containing your Terraform files. For example, imagine we want to provision an “a1.medium” EC2 instance with the following:

provider "aws" { region = "us-east-1" skip_credentials_validation = true skip_requesting_account_id = true}resource "aws_instance" "myserver" { ami = "ami-674cbc1e" instance_type = "a1.medium" root_block_device { volume_size = 100 }} ​If we add some extra storage (600GB of EBS), the cost increases to $155.52, as shown below:

$ infracost breakdown --path . Name Monthly Qty Unit Monthly Cost aws_instance.myserver ├─ Instance usage (Linux/UNIX, on-demand, a1.medium) 730 hours $18.62 ├─ root_block_device │ └─ Storage (general purpose SSD, gp2) 100 GB $10.00 └─ ebs_block_device[0] ├─ Storage (provisioned IOPS SSD, io1) 600 GB $75.00 └─ Provisioned IOPS 800 IOPS $52.00 OVERALL TOTAL $155.62 Infracost can also calculate usage-based resources like AWS Lambda. Let’s see what happens when we swap the EC2 instance for serverless functions:

provider "aws" { region = "us-east-1" skip_credentials_validation = true skip_requesting_account_id = true}resource "aws_lambda_function" "my_lambda" { function_name = "my_lambda" role = "arn:aws:lambda:us-east-1:account-id:resource-id" handler = "exports.test" runtime = "nodejs12.x" memory_size = 1024} Running infracost breakdown yields a total cost of 0 dollars:

$ infracost breakdown --path . Name Monthly Qty Unit Monthly Cost aws_lambda_function.my_lambda ├─ Requests Monthly cost depends on usage: $0.20 per 1M requests └─ Duration Monthly cost depends on usage: $0.0000166667 per GB-seconds OVERALL TOTAL $0.00 That can’t be right unless no one uses our Lambda function, which is precisely what the tool assumes by default. We can fix this by providing an estimate via a usage file.

We can create a sample usage file with this command:

$ infracost breakdown --sync-usage-file --usage-file usage.yml --path . We can now provide estimates by editing usage.yml. The following example consists of 5 million requests with an average runtime of 300 ms:

```

usage.ymlresource_usage: aws_lambda_function.my_lambda: monthly_requests: 5000000 request_duration_ms: 300

`` We’ll tell infracost to use the usage file with--usage-file` to get a proper cost estimate:

$ infracost breakdown --path . --usage-file usage.yml Name Monthly Qty Unit Monthly Cost aws_lambda_function.my_lambda ├─ Requests 5 1M requests $1.00 └─ Duration 1,500,000 GB-seconds $25.00 OVERALL TOTAL $26.00 That’s much better. Of course, this is accurate as long as our usage file is correct. If you’re unsure, you can integrate Infracost with the cloud provider and pull the utilization metrics from the source.

Git diff for costsInfracost can save results in JSON by providing the --format json and --out-file options. This gives us a file we can check in source control and use as a baseline.

$ infracost breakdown --path . --format json --usage-file usage.yml --out-file baseline.json We can now compare changes by running infracost diff. Let’s see what happens if the Lambda execution time goes from 300 to 350 ms:

$ infracost diff --path . --compare-to baseline.json --usage-file usage.yml~ aws_lambda_function.my_lambda +$4.17 ($26.00 → $30.17) ~ Duration +$4.17 ($25.00 → $29.17)Monthly cost change for TomFern/infracost-demo/devAmount: +$4.17 ($26.00 → $30.17)Percent: +16% As you can see, the impact is a 16% increase.

Running Infracost on CI/CDWe’ve seen how this tool can help us estimate cloud costs. That’s valuable information, but what role does Infracost take in continuous integration? To answer that, we must understand what infracost comment does.

The comment command takes a JSON file generated by infracost diff and posts its contents directly into GitHub, Bitbucket, or Gitlab. Thus, by running Infracost inside CI, we make relevant cost information available to everyone on the team.

Infracost comment on the cost difference in a GitHub commit.​

How to run Infracost on Semaphore⚠️ Before going into pipeline configuration, commit any files you’ve created into the repo. Also, if this is your first time using Semaphore, I suggest signing up for a free Infracost.io account and going through the getting started guide.

In this section, we’ll add two cost-control jobs to our CI pipeline. They will:

  • Analyze pull requests and post a comment in the conversation with the cost difference.
  • Comment on every commit that changes the infrastructure. The job will fail if it breaks any cost policy rules we establish.

Infracost preventing the deployment of resources exceeding a policy.​

For this section of the tutorial, you’ll need to create a GitHub token (choose the classic token) or a Bitbucket app password with read+write access to the repository.

We’ll save the Git provider password or token as a Semaphore Secret. To do that, go to your Organization menu in the top right corner and click Settings. Then, go to Secrets > Create new.

Define the following secret environment variables:

  • INFRACOST_API_KEY: you can retrieve it by running infracost configure get api_key in your machine.
  • GITHUB_API_KEYor BITBUCKET_API_KEY: an API token with read and write access to the repository. Note that, for Bitbucket, the key takes the form of username:api-key.

Semaphore Secret with Infracost and Bitbucket access tokens.Commenting commitsWe’ll begin by adding a job that comments on every commit changing a Terraform file.

Infracost comment on GitHub.​

To do so, open or add your project to Semaphore. To keep things simple, we’ll assume we already have a CI pipeline that builds and tests the project.

Our starting pipeline builds and tests the code.​

Add a new block with the following commands:

curl -fsSL https://raw.githubusercontent.com/infracost/infracost/master/scripts/install.sh | shcheckoutinfracost diff --path . --usage-file usage.yml --compare-to baseline.json --format json --out-file /tmp/infracost-diff-commit.jsoninfracost comment github --path=/tmp/infracost-diff-commit.json --repo=$SEMAPHORE_GIT_REPO_SLUG --commit=$SEMAPHORE_GIT_SHA --github-token=$GITHUB_API_KEY --behavior=update Let’s see what the job does:

  • The first two commands install Infracost and clone the repository into the CI Machine.
  • The third line compares the current costs with the ones stored in baseline.json (which should have been already committed to the repository).
  • The last line compares the changes and posts a comment to GitHub.

If you’re using Bitbucket instead of GitHub, the commands should be:

curl -fsSL https://raw.githubusercontent.com/infracost/infracost/master/scripts/install.sh | shcheckoutinfracost diff --path . --usage-file usage.yml --compare-to baseline.json --format json --out-file /tmp/infracost-diff-commit.jsoninfracost comment bitbucket --path=/tmp/infracost-diff-commit.json --repo=$SEMAPHORE_GIT_REPO_SLUG --commit=$SEMAPHORE_GIT_SHA --bitbucket-token=$BITBUCKET_API_KEY --behavior=update Remember to enable the secret you created earlier to ensure that the job has access to your API Keys.

Add a new block with the commands and enable the infracost secret. This job does not need to depend on any others in the pipeline.​

Conditional executionOur infracost job does not need to run on every commit. Only when a Terraform file changes, which we can detect with change-based conditions.

To turn on conditional execution, open the Skip/Run conditions section of the block and type: change_in('/**/*.tf') or change_in('/**/*.tfvars') so the job does not run unless a file ending with a tf or tfvars extension changes in the codebase.

With this condition, we only run the job when a Terraform file changes.​

⚠️ If your project’s main branch is not called master, you need to provide additional options. For example, if the main trunk is called main use: change_in('/**/*.tf',{default_branch: 'main'}) or change_in('/**/*.tfvars',{default_branch: 'main'}).

Commenting on pull requestsInstead of comparing the costs against a baseline, we can compare changes across branches on pull requests. This will give the reviewer a summary of the new costs.

Comments on pull requests give the reviewer insights on how the change impacts costs.​

To add comments on pull requests, we’ll create a new block with the following commands:

curl -fsSL https://raw.githubusercontent.com/infracost/infracost/master/scripts/install.sh | shcheckoutgit checkout masterinfracost breakdown --path . --format json --out-file /tmp/infracost-master.jsongit checkout FETCH_HEADinfracost diff --path . --format json --compare-to /tmp/infracost-master.json --out-file /tmp/infracost-diff-master.jsoninfracost comment github --path=/tmp/infracost-diff-master.json --repo=$SEMAPHORE_GIT_REPO_SLUG --pull-request=$SEMAPHORE_GIT_PR_NUMBER --github-token=$GITHUB_API_KEY --behavior=update Like in the previous job, we use infracost comment to post a comment, but this time we reference a pull request number and compare changes between the trunk and the committed branch.

To finish configuring the block:

  1. Enable the infracost secret
  2. Set the run condition to pull_request =~ '.*'. As before, we can enable change detection with: pull_request =~ '.*' and (change_in('/**/*.tf') or change_in('/**/*.tfvars')).

The pull request comment job does not depend on any other blocks in the pipeline.​

Check out Semaphore’s built-in support for monorepos to learn more about this feature.

Working with monoreposYou will likely have separate Terraform files for each subproject if you work with a monorepo. In this case, you should add an infracost config file at the project’s root. This allows you to specify the project names and where Terraform and usage files are located. You can also set environment variables and other options.

```

infracost-config.ymlversion: 0.1projects: - path: dev usage_file: dev/infracost-usage.yml env: NODE_ENV: dev - path: prod usage_file: prod/infracost-usage.yml env: AWS_ACCESS_KEY_ID: ${PROD_AWS_ACCESS_KEY_ID} AWS_SECRET_ACCESS_KEY: ${PROD_AWS_SECRET_ACCESS_KEY} NODE_ENV: production

`` When the config file is involved, you must replace the--pathargument with--config-file` in all your commands.

Setting up policiesOne more trick Infracost has up its sleeve is enforcing policies. Policies are rules that evaluate the output of infracost diff and stop the CI pipeline if a resource goes over budget. This feature allows managers and team leads to enforce limits. When the policy fails, the CI/CD pipeline stops with an error, preventing the infrastructure from being provisioned.

When a policy is in place, Infracost warns us if any limits are exceeded.​

Infracost implements policies using Open Policy Agent (OPA), which uses the Rego language to encode policy rules.

Rego has a ton of features, and it’s worth digging in to learn it thoroughly, but for our purposes, we only need to learn a few keywords:

  • deny[out] defines a new policy rule that fails if the out object has failed: true
  • msg: defines the error message shown when the policy fails.
  • out: defines the logic that makes the policy pass or fails.
  • input: references the contents of the JSON object generated with infracost diff.

The following example shows a policy that fails when the total budget exceeds $1,000:

```

policy.regopackage infracostdeny[out] { # define a variablemaxMonthlyCost = 1000.0msg := sprintf("Total monthly cost must be less than $%.2f (actual diff is $%.2f)",[maxMonthlyCost, to_number(input.totalMonthlyCost)],) out := { "msg": msg, "failed": to_number(input.totalMonthlyCost) >= maxMonthlyCost }}

``` This is another example that fails if the cost difference is equal to or greater than $500.

package infracostdeny[out] { # maxDiff defines the threshold that you require the cost estimate to be below maxDiff = 500.0 msg := sprintf( "Total monthly cost diff must be less than $%.2f (actual diff is $%.2f)", [maxDiff, to_number(input.diffTotalMonthlyCost)], ) out := { "msg": msg, "failed": to_number(input.diffTotalMonthlyCost) >= maxDiff }} You can experiment and try several examples online on the OPA playground. To enforce a policy, you must add the --policy-path option in any of the infracost comment commands like this:

curl -fsSL https://raw.githubusercontent.com/infracost/infracost/master/scripts/install.sh | shcheckoutinfracost diff --path . --usage-file usage.yml --compare-to baseline.json --format json --out-file /tmp/infracost-diff-commit.jsoninfracost comment github --path=/tmp/infracost-diff-commit.json --repo=$SEMAPHORE_GIT_REPO_SLUG --commit=$SEMAPHORE_GIT_SHA --github-token=$GITHUB_API_KEY --policy-path policy.rego --behavior=update ConclusionThe power to spin up resources instantly is a double-edged knife: a typo in a Terraform file can be a costly mistake. Staying proactive when managing our cloud infrastructure is essential to sticking to the budget and avoiding nasty surprises at the end of the month. If you’re already automating deployment with continuous deployment and managing services with Terraform, you may as well add Infracost to the mix to make more informed decisions and impose spending limits. Setting this up takes only a few minutes and can save thousands of dollars down the road.

Thanks for reading!

The post Taming Cloud Costs with Infracost appeared first on Semaphore.

View Details

Over the past years, table-driven tests have become a popular choice for unit tests within the Go community. They facilitate testing a function with a variety of inputs: A table-driven test consists of multiple rows of given inputs and expected outputs. You can keep these tests clean and efficient by following a set of simple guidelines. By “efficient”, I mean testing as many inputs as possible with the least amount of overhead.

What is a table-driven unit test?This is what a table-driven unit test should look like: A function sum(a, b int) int that takes two integers and returns their sum is tested with two test cases. A test case is represented by an anonymous struct consisting of two given input arguments and the expected result. Each test case is mapped against a unique name for that test case.

func TestSum(t *testing.T) { tests := map[string]struct { a int b int expected int }{ "10+5": { a: 10, b: 5, expected: 15, }, "2+2": { a: 2, b: 2, expected: 4, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { actual := sum(test.a, test.b) if actual != test.expected { t.Errorf("sums don't match: expected %v, got %v", test.expected, actual) } }) }} After declaring the test cases via the tests map, the test function iterates over all the map entries, i.e. the test cases. The sum function is called in each iteration using the input arguments of the current test case. If the actual result doesn’t match the result expected by the test case, the test fails with an error message.

The example above is relatively simple, but there are some interesting details to it. In fact, this test function already applies a number of suggestions from this article.

Maps instead of slicesSometimes, test cases in a table-driven setup contain a name field to uniquely identify and describe the test case. These test cases are then stored in a slice.

tests := []struct{ name string a int expected int} While this approach works well from a technical point of view, storing the test cases inside a map is preferable. When a lot of complex test cases start occupying too much screen space, it’s convenient to collapse individual test cases within the IDE. When using a plain slice, the test name will disappear, but remain for the maps. Also, besides being easier to navigate, a map clearly separates the test name from its fixtures.

tests := map[string]struct { a int expected int}

Another nice side effect of using a map is an undefined iteration order. Test cases might or might not be executed in the same order by the Go runtime, exposing faulty test setups where tests only pass when executed in a certain order. This can happen when there’s accidentally some state involved.

Comparing valuesThe example at the beginning only tests and compares primitive values using a builtin operator. When dealing with more complex comparisons, especially with comprehensive custom structs, this approach usually is too cumbersome. A library like Google’s go-cmp provides a convenient way for checking such values for equality.

Not only does go-cmp compare the values, it also returns the diff between the expected and the actual value as a string. Performing a comparison is simple – if the diff string is not empty, the values are different.

if diff := cmp.Diff(15, 20); diff != "" { t.Errorf("sums don't match: %v", diff)} Because the two provided arguments aren’t equal, the library reports a diff where - indicates the expected value, and + indicates the actual value.

sums don't match:-: 15+: 20 SubtestsInstead of running the test cases directly inside the for loop, our example uses the Run method for executing the test case as a so-called subtest.

These subtests change the behavior of the test function: Even if one or more of the test cases fail, all the other test cases will still be executed. A complete list of failing test cases (or a list of failing and passing test cases when the -v flag is used) will be printed when running a test function using subtests. In contrast, executing a failing test case directly inside the loop would cause the entire test function to exit, omitting the remaining test cases.

--- FAIL: TestSum (0.00s) --- FAIL: TestSum/8+8 (0.00s) sum_test.go: sums don't match: -: 16 +: 20 --- FAIL: TestSum/20+20 (0.00s) sum_test.go: sums don't match: -: 40 +: 50 There is no need to print the test name in the error message when a test fails, because the test name has already been passed to Run. The aforementioned list of test cases will include the respective test names.

Another benefit of subtests is the ability to only run a particular test case. The following command, for example, exclusively runs the 10+5 subtest:

go test -run="TestSum/10+5" Being able to run a specified subtest facilitates debugging that particular test and saves execution time.

Logging and failingWhen an actual value doesn’t match an expected value in a comparison, there are two options. Either an error message is logged using Errorf or the test is terminated using Fatalf.

The first option only logs the error and marks the test case as failed, but proceeds with the test function and its consequent checks. The latter option cancels the entire test case immediately upon failure. It is good practice to use these two options considerately. By default, when a check fails, it is advisable to only log the failure using Errorf and continue with the remaining checks. Only if the checks are a precondition for subsequent checks and proceeding wouldn’t make any sense, Fatalf should be used to fail immediately.

if len(actual) != len(test.expected) { t.Fatalf("lengths don't match: expected %v, got %v", len(test.expected), len(actual))}for i, value := range test.expected { if actual[i] != value { t.Errorf("values at index %d don't match: expected %v, got %v", i, value, actual[i]) }} The test above aims to compare the contents of two slices, test.expected and actual. It does so by comparing the values in a loop. Accessing the indexes is only safe if the lengths of the slices are the same, so the test immediately fails with Fatalf if this precondition isn’t met.

If the lengths are the same, the values within the slices are compared index by index. An error is logged using Errorf if the elements don’t match, which will mark the test as failed but continue with the comparisons within the loop.

Helper structsA test case can become quite large for functions with many parameters or many dependencies that need to be set up. To keep these complex test cases clear and manageable, it might make sense to outsource the input parameters and the expected results into their own helper structs.

func TestSum(t *testing.T) { type Given struct { a int b int } type Expected struct { sum int } tests := map[string]struct{ given Given expected Expected }{ // Test cases ... } // Test logic ...} The given input parameters are stored in a dedicated Given struct, and the expected result is stored in an Expected struct. The test case is still an anonymous struct but only contains these two fields. Of course, this approach is better suited for test cases that have a higher complexity than the TestSum example.

Note that both helper structs are defined inside the TestSum function. That way, a Given and an Expected type can be defined for every test function in the package. Declaring a third struct for storing mocked dependencies can be helpful, too.

Splitting functionsTable-driven tests are a good indicator for the complexity of a function. Test cases getting too convoluted is a sign that the function that needs to be tested either has too many dependencies or too many duties. If your function retrieves data from a third-party service, forwards this data to another service, and writes the data into a local database at the same time, chances are that the function is doing too many things at once. In this case, the function should be split into multiple smaller functions with clearly separated concerns.

Splitting the function into smaller pieces will simplify the test cases for these functions, and it will lower the effort for adding new test cases. Both the function itself and the test cases will be easier to reason about.

The bottom lineTesting your functions using the table-driven approach enables the testing of a large number and variety of input arguments with comparably little overhead. However, table-driven tests don’t guarantee sound test setups and clean test functions. By following the guidelines demonstrated here, you will be able to keep your tests as clean and efficient as possible. For a full example, check out the table-driven-tests repository.

The post Writing Clean and Efficient Table-Driven Unit Tests in Go appeared first on Semaphore.

View Details

Page speed matters more than you think. According to research by Google, the probability of users staying on your site plummets as loading speed slows down. A site that loads in 10 seconds increases the bounce rate by a whopping 123%. In other words, speed equals revenue.

Source: find out how you stack up to new industry benchmarks for mobile page speed.How can we ensure that our pages are loading at top speed? The answer is to measure them regularly with Lighthouse and CI/CD.

Measuring page speed with LighthouseLighthouse is a page speed benchmark tool created by Google. It runs a battery of tests against your website and produces a report with detailed advice to improve performance.

Lighthouse running inside Google Chrome.⚠️ You might be surprised at the low scores Lighthouse presents. This is because the tool simulates mid-tier mobile devices on a 4G connection.

While Lighthouse’s primary focus is performance, it can assess other things like:

  • Accessibility: suggests opportunities to make the content more accessible. This covers ensuring that the page is optimized for screen readers, all elements have labels, or the site is browsable with the keyboard.
  • Best practices: checks for various sane practices that improve speed and security.
  • SEO: performs various checks to ensure that the page is SEO-optimized.
  • PWA: ensures the page passes progressive web application tests, which improves user experience on mobile devices.

4 ways of running LighthouseLighthouse is an open-source project that you can run in different ways:

  1. Since it is included in Google Chrome, you can run it directly from the browser. Click on More tools > Developer Tools and open the Lighthouse tab.
  2. If you have Node installed, you can run npm install -g lighthouse and run the tool in the command line like this: lighthouse https://semaphoreci.com
  3. You can include it in your code as a Node package.
  4. And finally, Lighthouse has a CI version you can run in your continuous integration. We’ll use this method to schedule periodical benchmarks.

Setting up Lighthouse CIIn order to run Lighthouse CI, you’re going to need the following:

  • Google Chrome.
  • A GitHub or Bitbucket account.
  • The LTS version of Node.

Let’s get started by installing Lighthouse CI with: npm install -g @lhci/cli

Next, create a config file called lighthouserc.js. The URL parameter is an array of websites to benchmark. We can adjust the numberOfRuns, as more runs yield more statistically-relevant results.

// lighthouserc.jsmodule.exports = { ci: { collect: { url: ['https://semaphoreci.com'], numberOfRuns: 1 }, upload: { target: 'filesystem', outputDir: 'reports' } }}; Bear in mind that, while Lighthouse is not particularly heavy on resources, setting the number of runs to a high value can make the job take too long to complete. Never forget that you want your CI pipeline to run in than 5 to 10 minutes, not more than that.

Now we can run Lighthouse with: lhci autorun. When finished, we’ll find a few files in the reports folder:

  • manifest.json: contains a list of the reports generated.
  • $url-$timestamp-report.html is the main report you can open with any browser.
  • $url-$timestamp-report.json: the same report in JSON version.

Using Lighthouse for non-functional testingLighthouse lets us configure limits for each category. When one of these values falls below a predefined value, the tool stops the CI pipeline.

For example, let’s say we don’t want the ratings of our site to fall below 80% in all categories. To achieve that, we need to add assertions to the config file:

// lighthouserc.jsmodule.exports = { ci: { collect: { url: ['https://semaphoreci.com', 'https://semaphoreci.com/blog'], numberOfRuns: 5 }, upload: { target: 'filesystem', outputDir: 'reports' }, // enforce limits assert: { assertions: { 'categories:performance': ['error', {minScore: 0.8}], 'categories:accessibility': ['error', {minScore: 0.8}], 'categories:seo': ['error', {minScore: 0.8}], 'categories:best-practices': ['error', {minScore: 0.8}] } } }}; Measuring page speed with CI/CDThe next step is to automate Lighthouse with CI/CD. And for that, we’ll need to create a code repository and a Node project.

  1. Create and clone a repository on GitHub or Bitbucket.
  2. Initialize a Node project with npm init
  3. Add Lighthouse CI as a dependency: npm add @lhci/cli
  4. Add a start script in package.json:

"scripts": {"start": "lhci autorun",}, Do a trial run with: npm start

When done, you should have the following files in your project:

├── lighthouserc.js├── node_modules├── package-lock.json├── package.json├── reports ├── manifest.json ├── semaphoreci_com-_-2022_10_26_12_58_56.report.html ├── semaphoreci_com-_-2022_10_26_12_58_56.report.json If everything checks out, commit your files to the remote repository.

Continuous benchmarking with CI/CDNow it’s time to run the project with Semaphore.

We’re going to schedule Lighthouse benchmarks with Semaphore. If you’ve never used Semaphore before, check the starting guide first.

Let’s create a CI pipeline that runs Lighthouse CI:

  1. Add your GitHub or Bitbucket repository to Semaphore.
  2. Select “Single Job” as a starting workflow and click on Customize.

  3. Type the following commands into the first block:

checkoutsem-version node 18cache restorenpm installcache storenpm start 4. Open the Epilogue section, which is always executed even when the job fails, and type the following commands to save the reports in permanent storage.

artifact push workflow reports**for** f **in** reports/*.html**;** **do** artifact push project "$f"**;** **done** 5. Start the workflow with Run workflow.

What did we get for all this work? We now have a pipeline that scans the website and throws an error if it fails to make the cut.

You can check all the reports in the project’s Artifacts tab.

The HTML report can be found in the project’s Artifacts tab.Scheduling and setting a retention policySemaphore will, by default, run the pipeline on every commit to the repository. This works great when you’re using Lighthouse during development. But we need more if we want to monitor a live page.

We need a scheduler:

  1. Open the Scheduler tab in your Semaphore project and Click New Scheduler.
  2. Define how often you want to run the checks.

The following example runs a check every day at 12 PM:

Setting up a daily schedule to scan the website.The final step is to configure a retention policy to delete old reports:

  1. Go to the Project Settings tab.
  2. Click on Artifacts
  3. Set retention policies for each file type in the reports folder.

The following example sets a 6-month retention for HTML reports and 1-month retention for the rest of the files.

Setting up a retention policy for reports.As a final touch, you can set up Slack notifications to get pinged when tests fail.

How to analyze the resultsOur setup works, but I see a couple of problems with it:

  • We must delve into the CI logs when the test fails.
  • We can only test the settings supported by Lighthouse CI.

If these are problems for you, let’s explore some ideas to improve the testing.

Setting up a dashboardThe Lighthouse CI project includes an optional dashboard that lets you browse historical data and find trends.

Lighthouse CI dashboard.Installing the dashboard requires a separate server and database. You’ll need a dedicated machine and persistent storage to save historical data.

The downside of this approach is obvious — you need to manage yet another server. But it may be worth doing if you have a lot of sites to analyze.

Previewing test results on SemaphoreIf installing a dedicated server is overkill, you can still get a better experience and more insights with Semaphore’s test reports. This feature allows you to visualize all tests at a glance.

Viewing page speed tests historical data with the test tab in Semaphore.This approach has the added benefit of letting you write your own tests. That means you can do all kinds of fine-grained validations. For instance, you can fail a test if the page doesn’t use HTTPS, uses deprecated Web APIs, or the project doesn’t include a PWA manifest.

To make this work, however, you’ll need to do a little bit of work:

  1. Open lighthouserc.js and remove all the assertions (or set them to warn instead of error) so the scanning job never fails.
  2. Install your favorite testing framework.. It must support the JUnit XML reports, so I recommend Jest with Jest-Junit.
  3. Write some tests, ensuring they produce JUnit reports. You can parse manifest.json and the JSON reports and make assertions depending on your needs.
  4. Add an audit action in package.json:

"scripts": {"start": "lhci autorun","audit": "jest --ci"}, 5. Push the changes to the repository. 6. Add a second job in your pipeline to audit the results.

checkoutsem-version node 18artifact pull workflow reportscache restorenpm run audit 7. Add the publish and collect commands as described in the test results documentation.

You can check a working example at my GitHub repository:

tomfern / lighthouse-wordpressNow you can preview how many tests failed on the Activity tab:

You can see how many tests failed at a glance.And you’ll find a detailed test summary report for each run in the Test tab:

You can sort the test results and get insights without having to read the output of the test jobs.Running Lighthouse in web developmentWe’ve only focused on testing an existing and running website. But a setup like this can perform non-functional tests during web development. You can, for instance, fail commits that make the site perform worse or break accessibility rules.

Lighthouse used as a quality and performance gate before deploying a static website.Time is moneyUsers are drawn to fast and responsive websites. The problem is that measuring page speed reliably is challenging since you cannot assume that everyone is on a fast connection and uses a top-tier device. With Lighthouse in your CI/CD pipeline, you can get results closer to real-life conditions and insights to help you continually improve.

Thanks for reading!

The post Measuring Page Speed with Lighthouse and CI/CD appeared first on Semaphore.

View Details

Elixir is a dynamic, functional language for building scalable and maintainable applications. The creation of Elixir was initiated by maintainers of the Ruby language and the Rails framework projects running on top of Erlang Virtual Machine.

The Concurrency Revolution appeared as a talk in the C/C++ Users Journal (23rd February, 2005). In this article, Herb Sutter wrote that: “the free lunch is over.” The trend of new CPU releases was changing from more speed to more cores.

Desktop applications didn’t change much but increasing adoption of SaaS (Software as a Service) systems saw different frameworks and tools debuting on the development stage. One of them was Rails, created by 37signals, creators of Basecamp.

Back in 2011, a young and very active developer was working with Ruby and Rails. But the problem with the technologies of the day wasn’t just correct use of multi-core CPUs: this young developer was worried about thread-safety.

Thread-safety

The first handicap for these old systems was the thread-safety. This issue can be illustrated with the following example:

class PageController < ApplicationController def index @@my_info = params[:info] sleep 0.1 if @@my_info == params[:info] render json: "ok #{info}" else render plain: "it should never happen!", status: 500 end endend The @@my_info variable is global. In a multi-threaded context, this code shouldn’t have any problems because each thread has its own copy, right? The answer is “NO” because concurrency in Ruby shares the global state.

This problem isn’t unique to Ruby. It is present in other languages like C, C++, Java, and Python where global state is shared between threads.

When our young developer grabbed a book called Seven Languages in Seven Weeks and discovered Erlang it was a trigger for him. Other people had tried to do what José Valim wanted to achieve. Languages like Reia started as an idea to provide Ruby on top of Erlang, but José, a very active developer of Ruby with significant contributions to Rails, decided to create something different and called it Elixir.

The key is immutabilityThe Erlang platform was created as a telecommunications platform. Such a platform needed to have the following features:

  • Fault tolerance: doesn’t mean try/catch in every line of code — that’s defensive programming. Fault tolerance is the ability to contain failures to a small part of the system and the ability to restart that small part without affecting the other parts of the system. Contrary to panic in Go or an unhandled exception in Java, Erlang’s design tries to prevent general application outages.
  • High Concurrency Level. BEAM is share-nothing. The heap memory for each process isn’t accessible to any other processes. This makes a system thread-safe by default.
  • Soft real-time. Because we don’t share memory and the variables are immutable, the garbage collector doesn’t “stop the world” to free memory. This makes it possible to write programs with soft, real-time requirements. It means the time to run specific code is fixed.
  • Hot-code upgrade. Thanks to the previous features it’s also possible to perform hot-code upgrades. By loading a new version of the module, subsequent calls to those functions invoke the new code. In turn, the old version remains in use for the duration of the previous invocations. Once all previous executions are finished, the old version of the module is unloaded.

What could Elixir distill from its Ruby and Clojure influence? José had in mind some cool features from Ruby and, taking advantage of the design of the new language, he decided to copy from other languages he knew.

José Valim always says the pillars of Elixir are productivity, extensibility and compatibility with Erlang.

ProductivityA good basis for productivity is avoiding repetitive writing and repetitive tasks. Avoiding the boilerplate. The syntactic sugar brought from Ruby helps with this. Languages like Java, C++, or even Erlang sometimes need to use templates for specific implementations, meaning code is repeated again and again. Ruby fought against this and it can be seen in Elixir as well. For example, the if construct is defined in Elixir as a macro:

if state >= 50 do do_this()else do_this_instead()end# it could be also:if state >= 50, do: do_this(), else: do_this_instead()# which is also this:if(state >= 50, do: do_this(), else: do_this_instead()) Another facility is the pipe operator. Borrowed from Clojure, the pipe operator helps us build a flow of data. For example, when we develop using Erlang and we need to process data with different functions we have two different solutions:

update_user(User, #{email := Email, address := Address}) -> User1 = change_user_email(User, Email), User2 = change_user_address(User1, Address), save_user(User2). Using Elixir we can perform the same code without intermediary variables:

def update_user(user, %{email: email, address: address}) do user |> change_user_email(email) |> change_user_address(address) |> save_user()end Productivity is more than source code shortcuts: it’s based on having first-class documentation and tooling like the mix and iex commands, the ExUnit framework for tests, or even the Hex repository for dependencies.

ExtensibilityOne of the first conferences where I met José Valim was the Erlang Factory in 2015 in Stockholm. He presented a concept about Erlang and its JSON libraries. Sometimes we found libraries we could use but didn’t match our use case completely. Using the Inversion of Control pattern might have helped but not as easily since the language didn’t provide mechanisms to facilitate it.

Just to illustrate this idea, imagine we create a JSON library that provides a method for serializing data: numbers, strings, lists, tuples, and maps. But what if we need a defined structure that should be serialized in a specific way?

The initial idea of most Erlang developers in this situation (even the creators) was to copy the JSON library and perform a modification to include their customizations. Not very extendable. To address this, José borrowed an idea from Clojure: protocols.

defprotocol JSON do @spec encode(t) :: String.t() def encode(value)enddefimpl JSON, for: BitString do def encode(string), do: ~s|"#{string}"|enddefimpl JSON, for: Any do def encode(any), do: to_string(any)end This way we can add this source code as a dependency and provide a new defimpl which uses our structure and indicates how to perform data encoding.

Compatibility with ErlangLast but not least is compatibility which includes: concurrency, distribution, and embracing BEAM. As I mentioned at the beginning, the paradigm of development has changed and developers are forced to play more and more with high load, concurrency, and distribution. Elixir takes advantage of the virtual machine around which it was created (BEAM) and handles concurrency as Erlang does because the solutions provided by this language are the correct ones.

I think that the following quote about Elixir from an Evan Miller article puts things well:

“I almost always leave with the impression that the designers did the “right thing”. I suppose this is in contrast to Java, which does the pedantic thing, Perl, which does the kludgy thing, Ruby, which has two independent implementations of the wrong thing, and C, which doesn’t do anything.”

All of the decisions made regarding concurrency and handling of resources from the virtual machine perspective were correct because big companies like Whatsapp created something similar to other products with much less effort. Indeed, Facebook was using C++ and Hack (a typed version of the PHP language) and they needed lots of very talented engineers to make it work. I always remember a quote from Robert Virding about Virding’s First Rule of Programming:

“Any sufficiently complicated concurrent program in another language contains an ad hoc informally-specified bug-ridden slow implementation of half of Erlang.”

Robert Virding

Why implement something new if that’s what BEAM (the Erlang virtual machine) does? Therefore, Elixir uses BEAM instead of trying to reinvent the wheel.

Companies, developers, and toolsElixir is gaining traction and there are more and more companies using it daily. The list of companies using it includes PepsiCo, Spotify, Discord, Pinterest, Toyota Connected, Bleacher Report, Financial Times, Adobe, BBC, BlockFi, PagerDuty, Slack, and so on.

One of the more frequent excuses against new technology is the problem of hiring. Hiring Elixir developers isn’t easy, but keeping in mind the number of open positions available developers, it’s normal and not something specific to Elixir. It’s difficult hiring a developer no matter what technology, but you can always train your developers in-house. Hiring juniors or people willing to learn the technology and providing them the tools: books, courses, and training sessions, can be a good option.

We usually configure our build pipeline with the tools we use in order to ensure that the internal quality is good enough. These tools include:

  • Version control system. The clear winner in this category these days is git. If you are a developer, I don’t need to say more.
  • Continuous integration system. Here we have different alternatives. There is a lot on the market and depending on your needs you can choose one or another. Take, for instance, SemaphoreCI: performant builds supporting your stack with detailed reporting and important security features.
  • Continuous deployment system. In addition to continuous integration, it’s a great idea to set up auto-deployment of every change the moment it’s developed and passes the checks.

Elixir helps us with these because it:

  • Has a configuration for each different stage: dev, test, and prod.
  • Provides a way to perform releases using its default tooling (mix).
  • Has plenty of tools for ensuring good code quality: tests (ExUnit), coverage, dialyzer, credo, doctor, and sobelow.
  • Has a great log system (logger).
  • Has observability tools available in its ecosystem (telemetry).

As already stated, one of the pillars of Elixir is productivity and good tooling spawns productivity and team performance.

ConclusionElixir is mature and battle-tested in creating great software and providing a coherent, performant, and reliable development system to the delight of developers around the world. Many companies rely on Elixir and its features to provide robustness to their users. What’s stopping you from trying it out?

The post 5 Reasons to Use Elixir in Production appeared first on Semaphore.

View Details

Machine learning is everywhere. The state-of-the art of the field is advancing at an astronomical pace. GPT-3 can answer significantly complex questions, Stable Diffusion can generate incredible images from just words, and the list goes on. It’s nothing short of amazing.

But while the state of the art is being pushed daily, the long tail (e.g. the industry) is struggling to keep up. Most Machine Learning projects still have a significantly high failure rate. Data Science and ML teams are under increasing pressure to deliver successful Machine Learning models to the rest of the organization. Often, that delivery comes in the form of a microservice.

A Machine Learning microservice is an API that serves the ML team’s model. Designing a good Machine Learning microservice can be a challenging process. One of the most challenging aspects is keeping prediction latency down. Often, there will be requirements on how fast predictions need to be served, especially if the model is mission-critical.

In this article, we’ll go over 4 techniques that Machine Learning practitioners can leverage to scale their Machine Learning microservices. These techniques allow a microservice to more easily scale to thousands of users.

1. Leverage the CloudThe API is working on the development machine. Awesome. It’s now time to deploy it to production. Chances are that some sort of Public Cloud (e.g. AWS, GCP, or Azure) is available.

Choosing exactly which Cloud Service to use to deploy your microservice can make a big difference.

Image credits: Customers Love SolutionsThere are 5 major types of services:

  • SaaS: (not relevant for this article).
  • FaaS: Function as a Service. You give a function to the Cloud vendor, and they take care of the rest (e.g. Azure functions, AWS Lambda).
  • PaaS: Platform as a Service. You give a container to the Cloud vendor, and they take care of the rest (e.g. Cloud Run, AWS Beanstalk).
  • IaaS: Infrastructure as a Service. You rent a machine from the Cloud vendor (e.g. EC2 on AWS).
  • On-site: (not relevant for this article).

All of these have pros and cons. Generally, the more control you have over your infrastructure, the more expensive it gets (e.g. EC2 is more expensive than Lambda). However, if you rent an EC2 machine, that probably means that the machine is always on. One big advantage of PaaS and FaaS services is that they are elastic (e.g. you don’t have to pay if nobody is using your API). When using FaaS and PaaS services, the Cloud provider will automatically scale your microservice depending on traffic. If you are renting a virtual machine to deploy your service, you are limited to that single machine, and will be billed all the time, even if you are not making any predictions.

Generally, it’s a good idea to start simple, and gain more and more control over infrastructure as your needs evolve. This means that you should start by leveraging the most Cloud Native services first (e.g. start with FaaS, and move towards IaaS as your needs evolve).

Here’s an example:

  1. Design an app as a function.
  2. Deploy the API to Google Cloud Functions (FaaS).
  3. Example problem: permanent storage is needed, and Google Cloud Functions don’t offer it.
  4. Redesign the app as a container.
  5. Deploy the API to Google Cloud Run (PaaS).
  6. Example problem: a GPU is needed, and Google Cloud Run doesn’t offer it.
  7. Redesign the app to leverage GPUs.
  8. Deploy the API to Google Compute Engine (IaaS).

You can check out compare cloud to find the equivalent services for your Cloud provider.

Choosing the right microservice platform will make sure that your API can scale to the right requirements, and that you design the most efficient service possible.

2. ParallelizeLike most Machine Learning practitioners, chances are that you’re designing your microservice in Python. Even though Python is thought of as a slow programming language, there are some lesser-known techniques that can make it significantly faster.

There’s a high likelihood you can parallelize a process in your microservice and deliver predictions faster. Generally speaking, there are 3 main concurrency APIs that you can leverage in Python: Multi-processing, Multi-threading, and AsyncIO. Which one you choose largely depends on what type of task you want to parallelize. IO-bound tasks (e.g. calling an external API) should leverage threading or AsyncIO, while CPU-bound tasks (e.g. Linear Algebra) should leverage multi-processing.

Here’s an API decision tree from one of the best concurrency resources out there: superfastpython.com

Image credits: SuperFastPython.comLet’s take a look at a very simple example: we get some data from our user, fetch some external data, and then make a prediction on that fetched data:

from fastapi import FastAPIfrom pipeline import modelapp = FastAPI()@app.post("/predict/")async def predict(items): item_data_array = [] for item in items: item_data = fetch_item_data(item) # <- IO BOUND TASK item_data_array.append(item_data) predictions = model.predict(item_data_array) return predictions Since fetch_item_data is a function that calls an external API (e.g. an IO-bound task), we can parallelize it using threads. This decreases the readability of our code, but significantly increases speed:

from fastapi import FastAPIfrom pipeline import modelimport concurrent.futuresimport asyncioimport functoolsapp = FastAPI()@app.post("/predict-fast/")async def predict_fast(items): item_data_array = [] # less readable, but significantly faster with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: loop = asyncio.get_event_loop() futures = [ loop.run_in_executor( executor, functools.partial( fetch_item_data, item, ), ) for item in items ] for r in await asyncio.gather(*futures): item_data_array.append(r) predictions = model.predict(item_data_array) return predictions Next time you design your microservice, consider parallelizing some of the tasks to increase scaleability.

3. GPU-based inferenceAs models are getting larger, and datasets are getting bigger, the need for GPUs is quickly increasing. Training on a GPU regularly means a 10x (!) decrease in training time for your model. Popular frameworks, such as Hugging Face for NLP, are even designed to leverage multiple GPUs.

Most ML engineers should be pretty familiar with training models on GPUs. But what about inferencing? It’s also possible to leverage a GPU when making predictions. Hugging face, for example, uses a method called “Int8 mixed-precision matrix decomposition”. This works by reducing the mathematical precision of some model weights without impacting the accuracy of the model.

For example, this code:

model_name = "bigscience/bloom-2b5"model_8bit = AutoModelForCausalLM.from_pretrained(model_name, device_map="auto", load_in_8bit=True) loads the optimized ‘8-bit’ version of the bigscience/bloom-2b5. If a GPU is available, the device_map=auto keyword argument would automatically make use of it.

It’s worth investigating if your framework supports inferencing on GPUs, but there are some caveats:

  • Having GPUs available at runtime is not so common. For example, Google Cloud Run does not support GPUs, but AWS lambda looks like it does.
  • GPUs are expensive, especially if you don’t shut them off when you finish. So be aware of that.
  • Make sure you choose the right hardware. Multiple CPU-based inferences in parallel could have similar performance at a lower cost than a GPU-based inference.

4. Use a batching endpointIt’s common for a Machine Learning microservice to serve an endpoint to make predictions. This endpoint has several responsibilities:

  • Validating data that is passed to the API
  • Cleaning and formatting the data to be used in the model
  • Making a prediction with the model
  • Formatting the result
  • Returning the result to the API caller

Here’s a simple example:

from fastapi import FastAPIfrom pipeline import model, clean_data, format_data, data_is_validapp = FastAPI()@app.post("/predict/")async def predict(item): if not data_is_valid(item): return {"message": "data not valid"} item = clean_data(item) predictions = model.predict(item) output = format_data(predictions) return output Being able to scale means that your API endpoint gracefully handles simultaneous invocations in very short intervals. This means that we need to take advantage of anything we can do to improve our latency. To do this, we can leverage the fact that Python is mostly based on the C programming language. Numpy, for example, does this by further extending Python to take advantage of the C programming language. As a result, it’s very fast.

The speed of Numpy (and other scientific libraries) really shines when dealing with more than one element. For example, model inference, matrix multiplication, or pandas dataframe computations are much faster if done in batch. One way of getting quick gains in scaling is to create a batch prediction endpoint for a service:

from fastapi import FastAPIfrom typing import Listfrom pipeline import model, clean_data, format_data, data_is_validapp = FastAPI()@app.post("/batch-predict/")async def predict(items: List[str]): items = list(set(items)) # <- remove duplicates items = [i for i in items if data_is_valid(i) == True] # <- leverage list comprehensions items = clean_data(items) # <- probably has some numpy or pandas predictions = model.predict(items) # <- faster and more efficient than calling .predict N times outputs = format_data(predictions) return outputs By creating a batch prediction endpoint, you are doing a lot of the work that would normally be up to the API caller. However, you are also improving the efficiency of your microservice. If your app scales well, calling the batch-predict endpoint once will be much faster than calling the predict endpoint N times.

5. BonusHere are 3 bonus tips to help improve the performance of your service:

  • Look into pruning: pruning trims insignificant weights from your model, making it smaller and likely faster to operate.
  • Optimization frameworks: leveraging Apache TVM and ONNX can significantly optimize both the speed and size of your model.
  • Leverage Python: we’ve talked about Threads and Processes, but tools like lru_cache can also help speed up your service, especially if you are making external API calls. Staying up-to-date with the latest software can also make your application faster. Python 3.11, for example, is 10-60% faster than Python 3.10.

ConclusionThere’s still no way to automagically scale your custom Machine learning microservice. Nevertheless, this article has given you a wide range of solutions that will help you easily scale a microservice to thousands (or even millions) of users.

The best technique to optimize really depends on your use case. Are costs a concern? Then stay away from GPUs. Do you receive a lot of repeated calls? Then leverage a batch endpoint. Is your Cloud Function (e.g., FaaS) slow? Perhaps it’s time to choose another service, like PaaS (e.g., Cloud Run).

It’s not worth optimizing just for optimization sake. Remember, perfect is the enemy of good. Focus on serving your users, and optimize when you encounter a performance bottleneck. Hopefully, the tips above are a good menu to choose from.

The post 4 Ways to Scale Your Machine Learning Microservice appeared first on Semaphore.

View Details

The prominence of venture capital in the software industry continues to shape how companies grow, set their budget and goals, and model their teams. It even affects how developers perceive their abilities. Nevertheless, Ruby on Rails creator and CTO of 37signals (Basecamp & HEY) David Heinemeier Hansson invites those looking to start a company in the industry to become skeptical of the need for financing and encourages them to apply the practices that have led his (and many others) companies to success.

Edited transcriptionDavid Heinemeier Hansson is a software developer and author. He is known for being the creator of Ruby on Rails, an open-source framework for web application development. David is also the co-owner and CTO of 37signals, a web software development company, and one of the creators of Basecamp, a project management platform released in 2004.

When David launched Basecamp, many features that are now a given in software development demanded more steps. “We were facing such banal things as ‘how do you charge a credit card?’ There was no stripe back then. You needed your own merchant agreement with a bank that was going to process it, and they were going to do a custom bespoke evaluation of your business, whether you were worth the risk,” he recalls. In like manner, Ruby on Rails, as a framework for working with Ruby, was extracted from their labor coding Basecamp, at that time in which, says David, Ruby “was not used in any wide sense commercially and certainly didn’t have a framework that just made it easy to get going”.

After building Ruby on Rails as tooling for using Ruby, David and his team released it as open-source software. He found it “really gratifying to see in those early years how quickly people realized, ‘okay, here’s a step change.’ ” Ruby on Rails allowed developers to create web applications faster and more straightforwardly and became widely adopted for web development, with thousands of people contributing to its code.

How venture capital and bootstrapping define companiesTo David, part of the notoriety Ruby on Rails achieved back then was due to the timing: “new businesses were springing up all the time” as tech companies were leaving the dot-com bubble behind and receiving with open arms the idea of web 2.0 as a redemption away from the financial speculation that had fueled the bubble. This mix of skepticism toward investment capital on the one hand and expectations for the possibilities available in a new milestone in the digital age on the other defined what David understands was a healthy era for his company to grow in.

Similarly, just as a lack of money didn’t stop David’s projects, neither did the size of his team. The team behind building Basecamp was of merely four people, with David working on it part-time. Withal, David believes that remaining small boosted their productivity. “That ethos of productivity was something that left a very deep mark on Ruby on Rails, certainly left a deep mark on me in terms of setting the pace for what’s possible going forward,” he judges. Hence, David understands that building a great product does not demand having a huge team, infrastructure, or capital. Instead, startups can achieve great results by relying mostly on open-source tools and building the rest by themselves.

However, David has noticed that venture capital (VC) financing has gained terrain in the software development industry while diminishing the capacity of small and medium companies in favor of the business model of large ones. As a result, David sees that startups, despite their size, focus more on raising capital and enlarging their teams than on the actual product they are building and their profits. David traces back this tendency from the moment large software companies such as Facebook and Google “started sharing their technology, the kind of stuff that worked really well if you were a hundred thousand people or 50,000 people and then shoved it out as general purpose solutions that made sense to a startup of one, two, or three people.”

To David, the tooling big companies utilize cannot work for small companies organically. Further, the adoption of this tooling as the only way to success is why startups are bluntly looking to become larger and raise more money. Likewise, David believes that companies will create products for companies of their same size: “the kind of software that the companies with 5,000 employees are more built for are the kinds of software that companies of 5,000 or more would buy”.

In the VC business model, companies engage in a rat race for becoming larger and larger. These companies start small, but after validating their business idea and gaining enough traction they move upstream and start targeting larger, enterprise clients. David asserts that “for these kinds of companies, small and medium-sized businesses are just a stepping stone to get to validation”. This business cycle either concludes after the business is unable to sell to these enterprise clients or succeeds, yet they are acquired by other companies and the company ends up dismantled.

However, David believes credit availability in the industry is reverting to what it once was when Basecamp was released. He points out that the market implosion is showing wrong those who pursued working on red numbers and a sign of the fact that “there’s going to be a reset in these kinds of expectations that we can no longer approach technology and business as this area where the barrier of entry is the VC funding route.”

Hence, software development startups are bound to pick up the tools and means which were always at the reach of their hand. David believes bootstrapping will popularize and is destined to have a new golden age where companies will base their business model on profitability and the quality of their employees and customers exclusively. “I think you actually create healthier companies when you start them during recessions, they teach you to be more efficient with your capital, they restrain you; they put boundaries about the endless growth that you might otherwise get sucked into pursuing,” he concludes.

Generalist or specialist?In like manner, David sees that the specialization of the industry and the creation of more developer roles cause people to think only a large team made of specialists can do something meaningful. According to David, specialization in the skills and domain of developers, as well as the “complexity that has a build-up in many of the popular approaches to building software these days,” can lead to a “state of learned helplessness.” Developers might perceive themselves as incapable of grasping the whole picture of software development and, hence, as incapable of creating anything by themselves.

But David believes this is not the case. He points out that “much of the popular technology that people are using these days and have been using for the past at least ten years came out of businesses at the other end of the extreme in terms of how large they were and how they needed to work.” David does concede that large companies are probably more inclined to work with specialists (“You don’t have a company of 10,000 generalists,” he ironizes), but the whole industry cannot have a mindset that “essentially guards the productive capacity of individuals.”

David insists that small teams made of people who are willing to learn how to use tools outside their experience or area of expertise can be successful. Moreover, he reminds us that most web applications are essentially database applications, meaning “forms that submit things to databases and present that data back to you”. As such, he believes web development is not nearly as demanding as it might seem to be. Working with native applications, for its part, might be more complex, but this doesn’t mean the task is impossible to tackle.

Is web development outdated?In the task of “compressing complexity,” David recognizes that the software and hardware advances of the last decade had made developers’ work much easier. “Some of the workarounds that we used to do when we were dealing with the spinning rust plates inside hard drives are no longer relevant when you can take specialized systems and throw them away because generalized systems are now fast enough that they can solve the same problems,” he says.

Still, David recognizes that less and less often new technologies appear in web development. In turn, machine learning, AI, and even crypto are the vanguard of breakthrough technologies. Moreover, the effort it would take to create something new and helpful in web development will not have the same returns it would bring if put in one of the aforementioned vanguard domains. But David says that this shouldn’t be thought of as a limitation, but rather, as a feature that ensures web development is still required while remaining “a well-understood profession”.

The bottom lineDavid’s passion for programming is one of the main reasons he decided to keep his company small. “We’re not as small as we used to be, but I can fill my day with the things I really care about,” he admits. David encourages us to prevent burning out by finding life balance (or rather, work, life, and sleep balance) and making the business model compatible with a long journey. “You can absolutely build a great business in 40 hours or less per week. We’ve been doing it for 20 years. It’s immensely doable. Sometimes you have to sprint, but it should be a sprint where you stop afterward, catch your breath, and then don’t go for another one right after,” he affirms.

You can learn more about David’s work on his website and follow him on Twitter.

The post David Heinemeier Hansson on Finding the Right Balance Between Business and Technology appeared first on Semaphore.

View Details

Running a microservice infrastructure involves a wide variety of organizational and technical challenges, many of them regarding the communication between services. Traditionally, operations and developer teams take care of these challenges and implement their own solutions. But there is a more unified, standardized, and scalable way: service meshes.

This article introduces the concept of service meshes, the problems they solve, and the features they offer. As Kubernetes is a widespread and well-understood platform for running microservice infrastructures, we’re going to focus on service meshes built for Kubernetes.

Challenges in microservice environmentsWhile calls to other modules within a monolithic application are simple in-memory operations, calling a service in a microservice-based application is more complex. Communicating with another service entails real network calls, which inherently introduce their own class of problems.

Consequently, developers need to answer a variety of questions when building a microservice architecture:

  • Must the network communication between the services be encrypted?
  • How can a service make sure that it is communicating with the correct peer service?
  • Which services are allowed to communicate with each other? How can these rules be enforced?
  • How should resilience features such as retries or service timeouts be implemented?
  • What does traffic management look like, e.g. load balancing or routing traffic to a newer service version?

A service mesh is an infrastructure layer that addresses these problems in a holistic and standardized way. There are plenty of service meshes available, the most popular ones being Google’s Istio, HashiCorp’s Consul, and Buoyant’s Linkerd.

How service meshes for Kubernetes workAll popular service mesh implementations for Kubernetes follow the same fundamental idea: they deploy a network proxy next to each service instance. Note that my use of the term “service” here refers to the actual microservice, not a Kubernetes Service. These network proxies intercept the service’s network traffic, and they are the precise point where all service mesh features are enforced. To accomplish this, a service mesh has a control plane and a data plane.

Control plane and data planeThe data plane consists of a large number of proxy servers, each of them sitting next to a single service instance. The service mesh automatically injects those proxy servers into the Kubernetes pods of the service. In Kubernetes terms, the proxies are sidecar containers, thus they are also referred to as sidecar proxies.

Outbound requests of a service are intercepted by the proxy server within its own pod. The proxy then establishes an encrypted connection with the target service. Because inbound requests are intercepted as well, the actual connection will be established with the target service’s proxy. Once the connection is established, the service mesh features can be applied.

The control plane consists of components that are responsible for controlling the proxies in the data plane. It is aware of all services and the networking constraints imposed on them and provides this information to the sidecar proxies. In most cases, the control plane is a simple Kubernetes namespace.

AdvantagesThis approach comes with several advantages for the operation of existing services and the adoption of new ones.

First, networking policies and features are managed in a central place: the control plane. The services themselves are no longer in charge of networking security, resilience, observability, and traffic management. This means that such capabilities don’t need to be implemented within the services anymore, which enables developers to focus on business capabilities instead. Thus, building new services will become cheaper and less networking-heavy.

Also, the networking policies are first-class Kubernetes resources. They are not implemented as obscure application code within the service, rather they are defined in a declarative and uniform fashion. This means that the networking configuration is standardized across all services in the infrastructure and even across Kubernetes clusters. Making networking behavior transparent to other parts of the organization, such as operations teams, is a major step towards improved reliability.

DrawbacksThere are some drawbacks that should be considered when adapting a service mesh. Injecting a sidecar proxy into all Kubernetes pods has significant implications. The infrastructure is expanded by n proxy servers, where n is the number of service instances. Each proxy server consumes additional CPU and memory resources, and each service call will be extended by two proxy hops. This is considerable operational overhead.

Also, besides understanding their services and Kubernetes, developers also need to understand how the service mesh works. Figuring out whether a feature is already covered by the service mesh or needs to be implemented in the application, troubleshooting the application, setting up a deployment workflow for the service mesh itself – all of these require thorough knowledge about the service mesh.

FeaturesBy intercepting both inbound and outbound traffic, the service mesh has full control over inter-service communication. The exact feature set depends on the particular service mesh implementation, but there are capabilities that most service meshes have in common.

SecurityStandardized security policies are one of the greatest advantages of service meshes. There are three security aspects taken into account by service meshes: encrypted inter-service communication, service-to-service authentication, and authorization of inter-service communication.

Encrypted inter-service communicationService meshes provide encrypted inter-service communication via mutual TLS (mTLS). Typically, the service mesh assigns a unique certificate to each service. For outbound requests, the client service verifies the identity of the target service using the service mesh’s certificate authority. After the target service has successfully verified the certificate of the client service, an encrypted connection is established.

Requesting the certificate data from the certificate authority in the control plane is a time-consuming operation. To circumvent this, some service meshes deploy local node agents that keep an in-memory copy of the certificates that are kept up-to-date by the control plane. According to their vendors, this shortens the certificate checks to a few microseconds.

Service-to-service authenticationA connection encrypted via mTLS not only allows for secure communication, it also enables identity-based communication between services, so that a service is guaranteed to communicate with the expected peer service. To check this, each service is equipped with an X.509 identity and uses this identity to authenticate itself against other services.

Service-to-service authentication also paves the way for authorized inter-service communication.

Authorized inter-service communicationIt is common practice not to allow inter-service communication by default. Services that need to communicate with each other should be explicitly authorized to do so. In traditional setups, this is oftentimes checked manually within the service: client services have to authenticate themselves using a service token, and they have to be “whitelisted” by the target service for the communication to be permitted.

Services within a service mesh are uniquely identified using service-to-service authentication. This enables the service mesh to allow or disallow traffic between two defined services. For example, Consul uses intentions to authorize inter-service communication. The following Kubernetes manifest allows a service called web to communicate with an api service:

apiVersion: consul.hashicorp.com/v1alpha1kind: ServiceIntentionsmetadata: name: web-to-apispec: destination: name: api sources: - name: web action: allow A Kubernetes manifest is a declarative and standardized specification, as opposed to custom-built application code that performs these checks.

ResilienceA service mesh shifts all networking capabilities into their own infrastructure layer. This also means that resilience features can be handled by the service mesh – primarily retries and timeouts.

Retries and timeoutsRequested services may not respond within a defined timeout period or become unavailable. In these cases, it might make sense to retry the request. In a microservice environment, where everything happens via network calls, retries and timeouts are fundamental building blocks for resilience.

Implementing a proper retry mechanism isn’t a trivial task. The developer must decide how many retry attempts are appropriate, and how long the intervals between the retries should be. This is a difficult trade-off to make: on the one hand, the number of retries should be enough for the client to get an actual response; on the other hand, too many retries cause a flood of traffic that the target service can’t handle, making it unavailable again. These retry storms are a serious threat to any distributed system.

Some service meshes provide an intelligent and data-driven solution for finding a proper balance: for instance, Linkerd monitors the ratio between normal requests and retry requests. Retry requests are only allowed if this ratio is below a previously defined limit–the so-called retry budget. For a retry budget of 25%, the proxies will only perform retries until the extra load on the target service exceeds 25%. The service itself doesn’t notice these retries.

Fault injectionTo thoroughly test resilience and stability when a fault occurs, certain service meshes have built-in fault injection as a form of chaos engineering. Fault injection artificially increases the number of erroneous responses, exposing how the system copes with them.

Just as with automatic retries, fault injection doesn’t require any changes in the application code.

Traffic managementIntercepting network traffic empowers the service mesh to manage it. There are various kinds of traffic management that meet different demands.

Traffic routingService meshes are capable of routing traffic based on Layer 7 criteria such as HTTP methods, request headers, or path prefixes. Depending on these criteria, requests may be routed to different backends. In Consul, routing all HTTP requests with a payment path prefix to a service called payment would look as follows:

apiVersion: consul.hashicorp.com/v1alpha1kind: ServiceRoutermetadata: name: webspec: routes: - match: http: pathPrefix: /payment destination: service: payment Such routing would also be possible using Kubernetes itself. However, service meshes have built-in support for more complex use cases, such as canary deployments.

Traffic splitting and canary deploymentsAfter routing the traffic to a service, the service mesh can additionally split it. That is, the traffic can be split among two or more instances of the respective service, e.g. the first request goes to instance #1, while the next request goes to instance #2, and so on.

There are various criteria for how the traffic should be split, and which service instances should be considered as targets. For instance, weighting could be used for splitting and service versions could be used for target selection, e.g. 80% of the traffic should be routed to instances running version 1.0 of the service, and 20% of the traffic should go to instances running version 2.0.

This mechanism facilitates rollout strategies such as canary or blue-green deployments. In the aforementioned example, service version 2.0 could be tested on 20% of the users.

Traffic mirroringBesides splitting traffic between peer service instances, it can also be cloned (or mirrored) among service instances. These service instances don’t necessarily need to be in the same environment. For testing purposes, traffic from the production environment could be mirrored to a new service version in the staging environment. Testing new versions with real production workloads allows for much more confident releases and deployments.

ObservabilityBecause all inter-service communication is taking place within the data plane, the service mesh knows the behavior of the services. Thus, collecting service and network metrics is a breeze.

The majority of service meshes collect the “golden metrics” – request success rates, requests per second, and latency – and supply a built-in dashboard for this data. Where these metrics are sent to depends on the service mesh and can usually be customized.

Since a service mesh spans the entire microservice infrastructure, it is able to capture and visualize the topology of the infrastructure. Visualizing the topology along with configured inter-service communication can give helpful insights about the infrastructure.

ConclusionA service mesh promotes inter-service communication to a first-class citizen. Networking between services is no longer implemented manually as language-specific application code, potentially opaque to other teams. Instead, its behavior is clearly defined in a declarative, organization-wide, language-agnostic, and standardized way.

There is, however, a cognitive and operational overhead to service meshes and organizations have to carefully gauge whether they need to make this trade-off. In any case, inter-service communication is one of the most critical paths in any distributed system, and moving it into a dedicated layer does justice to it.

The post Service Meshes for Kubernetes: Unlocking Standardized Security, Resilience, and Traffic Management appeared first on Semaphore.

View Details

At Semaphore, we are always looking for ways to improve our authentication and security systems. That’s why we are excited to announce that we are now supporting OpenID Connect (OIDC), an open standard for authentication.

What is Open ID Connect?OIDC is an authentication protocol built on top of the OAuth 2.0 framework. It allows pipelines to securely authenticate with cloud providers, such as Google or AWS, or to connect to secret management tools like Hashicorp Vault. Semaphore pipelines can access resources on these platforms without creating and storing long-living credentials.

This allows:

  • Fine-grained control accessing resources on Cloud providers. For example, you can limit access to your production system only to pipelines running on the main branch.
  • A seamless connection between Semaphore CI and Cloud providers. For example, pipelines can directly assume AWS roles and inherit versatile policy options from AWS.
  • Eliminate the need for storing long-lived access credentials, and grant only short-lived access to your cloud resources from any given pipeline.

Establish a trust relationship between Semaphore and your Cloud ProviderTo access your cloud resources from Semaphore workflows, first, you need to establish a trust relationship between Semaphore and your cloud provider.

  1. Set up OIDC trust on your cloud provider to manage access between pipelines and cloud resources.
  2. In each pipeline, Semaphore presents an OIDC JWT token to the cloud provider with information about the project, branch, pull request name and number, job, pipeline, and workflow IDs.
  3. The Cloud provider validates the claims and provides a short-lived access token to fetch secrets, deploy your code, or to manage resources during the execution of the pipeline.

For details about setting up and using Open ID Connect with your preferred cloud provider, read the documentation page about connecting with OIDC.

Start using Open ID ConnectWe are excited to offer support for OIDC, and we believe it will greatly improve the developer experience and security of our application. OpenID Connect is available on the Enterprise plan.

Would you like to check out OpenID Connect for your organization? Feel free to contact us at support@semaphoreci.com, and we’ll be happy to help you.

Thank you for using Semaphore!

The post Introducing Support for OpenID Connect appeared first on Semaphore.

View Details

Grammar mistakes. Spelling errors. Tabs versus spaces. Searching through shell history to find the right command to run your tests.

If you’ve spent time doing any of these things, then you’ve probably felt like your time could have been better spent fixing bugs and writing documentation. After all, we can write programs and scripts to check spelling errors and enforce code style for us. The key piece is automating it, and CI/CD is a great place to run that automation. By dedicating a small amount of time now to define some common code review drudgery as CI/CD jobs, you’ll lift the burden of those tasks off yourself and fellow team members every time a new change arrives in your repository — no matter the programming language.

In this post, we’ll explore CI/CD jobs that are both widely applicable and generally useful that they can fit into any repository.

ProseWe often think that contributing to a repository solely takes the form of programming, but commits are often both code and written language. Prose can be crucial to help collaborators understand difficult concepts and crystallize tribal knowledge in well-documented places.

Written content can appear in a variety of forms, such as:

  • The well-known README.md file at the root of most repositories.
  • In a dedicated docs/ or similar directory with multiple files.
  • Scattered throughout code in the form of inline comments.

With so many files to check and varying levels of writing ability among team members, automated checks to validate spelling and other simple rules are prime candidates for CI jobs. That automation is useful for nearly any repository. A wide selection of tools means that jobs can range from simple spell-checking to high-level style guidelines to maintain documentation consistency.

One tool that addresses spelling and style is Vale: a system for validating that text conforms to a configurable set of rules. Integrating Vale into a CI/CD pipeline to enforce writing guidelines for documentation is an effective way to offload otherwise-tedious editorial work to a consistent system.

The first step to enabling Vale for a repository is to generate a .vale.ini file that defines the desired rules for Vale to use when it performs style checking. For example, place the following .vale.ini at the root of a repository to leverage proselint as well as write-good (two projects that offer pre-defined style rules).

StylesPath = stylesMinAlertLevel = suggestionVocab = BasePackages = proselint, write-good[*]BasedOnStyles = Vale, proselint, write-good Next, Install the vale executable. Vale is available for a variety of platforms, so choose the method native to your operating system. For example, use the command below to install Vale on OS X using homebrew:

brew install vale Or, use this command to install Vale on Windows:

choco install vale On Linux, you can install Vale with most package managers under the name vale.

Next, use the sync subcommand, which reads the .vale.ini file and populates the directory indicated in the StylesPath setting (in our case, styles/) with the desired styles noted under Packages.

vale sync Vale will download the necessary files to satisfy the configuration in .vale.ini. Once complete, you should commit .vale.ini as well as the styles directory so that they’re available later for CI/CD jobs to use.

git add .vale.ini stylesgit commit -m 'Add vale configuration' To perform checks locally, run vale against any text file, such as a README:

vale README.md Vale will exit non-zero upon finding errors, while warnings and suggestions are often stylistic. Here’s a snippet of an example warning:

5:157 warning 'be automated' may be passive write-good.Passive voice. Use active voice if you can. Programming-oriented syntax checkers seldom require additional configuration, but prose can often be subjective and may require more fine-tuning. For example, some technical terms may fail spell-checking:

51:22 error Did you really mean Vale.Spelling 'subcommand'? If you need to accept particular words as part of your repository’s vocabulary, add one word per line to the file styles/Vocab/Base/accept.txt and Vale will accept the indicated words as correctly spelled.

Integrating Vale into a CI/CD environment becomes an exercise in installing the vale executable and expressing a simple task that runs:

vale **/*.md . . . or a similar wildcard glob to enumerate the desired set of targeted files. We’ll explore simple methods to load vale into a testing environment near the end of this guide using a generalized pattern for executing scripted tasks.

Common code formattingOne of the most time-honored debates among programmers is tabs versus spaces. While the battle may rage between strangers, you and your team should have established formatting practices not only for indentation but for other syntax rules as well. For example, enforcing consistent line endings can alleviate noisy diffs between Linux and Windows editors.

Fortunately, the EditorConfig project establishes a single formatting method that most editors and IDEs honor. These settings can apply to nearly all source code regardless of language, making the .editorconfig file an excellent choice to reduce noisy commits arising from style disagreements or misconfigured editors.

EditorConfig supports a wide range of options, including:

  • Indentation style — whether with tabs or spaces — and indentation width for soft (space-based) indentation.
  • charset to enforce valid (or invalid) characters in matched files.
  • Options to catch unwanted trailing whitespace.
  • Flexible matching to selectively apply rules to specific files.

The .editorconfig file is typically used during writing inside an editor. Providing an .editorconfig as part of a code repository can ensure that all authors use the same rules for code and prose (as long as contributors enable EditorConfig features in their chosen editor).

To use EditorConfig, create an .editorconfig file at the root of your repository. Here’s an example configuration file:

root = true[*]end_of_line = lfinsert_final_newline = true[*.{json,yml}]indent_style = spaceindent_size = 2 This .editorconfig defines the following settings:

  • root = true instructs any editors and checkers to stop searching upward in the filesystem hierarchy for any parent .editorconfig files. Without this line, any .editorconfig files in parent directories would apply. You should include a line with the root setting to apply settings consistently on all developer systems.
  • For all file types ([*]), use line feed characters to terminate lines and ensure that a new line exists at the end of all files.
  • For all JSON and YAML files ([*.{json,yml}]), enforce the use of spaces for indentation, and use two space characters for each indent level.

After committing the .editorconfig file, all contributors should enable EditorConfig in their respective editors (instructions for a wide variety of programs is available on the EditorConfig home page). However, you may also define a CI job to ensure that its rules are consistently applied.

editorconfig-checker is a stand-alone application to check for consistency between an .editorconfig and its repository’s files. To install it, download a compiled release artifact from the project’s GitHub releases page or use the go command natively to fetch and compile the latest version.

go install github.com/editorconfig-checker/editorconfig-checker/cmd/editorconfig-checker@latest Once installed, use the editorconfig-checker command to check all files in the repository against your .editorconfig file:

$ editorconfig-checkerREADME.md: 17: Wrong indentation type(spaces instead of tabs) You and your team can refine formatting rules as you find a style that fits the desired tone for your documentation. Consult the EditorConfig home page for additional information about available settings.

EditorConfig and Vale are powerful, but we can take them further by providing a streamlined entry point for both CI jobs and regular users.

Helper scriptsEasily accessible scripts can help reduce friction for new users and regular contributors. New users can rely on a reliable interface for shared tasks, and returning committers can save effort by codifying frequently-used commands. In either case, providing a consistent mechanism is a great boon to any code repository.

The most important decision is choosing tools and commands that are accessible. There are numerous options to choose from: make is a nearly ubiquitous tool, whereas utilities like just offer more modern features at the cost of ensuring its presence on all systems. This tradeoff can vary between teams, so for these examples, we’ll use make because it’s nearly always already installed.

One of the most useful first-run experiences with a repository is to glean a list of possible commands at a glance. A default Makefile target for help makes this easily discoverable:

.DEFAULT_GOAL := help.PHONY: helphelp: ## Display this help text@awk 'BEGIN { FS = ":.*?## "; printf "Available targets:\n"; }; \/^[a-zA-Z\-_0-9]+:/ { printf "\t\033[36m%-20s\033[0m %s\n", $$1, $$2 }' \$(MAKEFILE_LIST) * .DEFAULT_GOAL means that running the singular make command will run the help target by default. * .PHONY indicates that “help” is the name of our target, but not the name of a file. * The awk command will parse the Makefile with logic to present its contents in a clear way. * $(MAKEFILE_LIST) is a variable that contains all Makefiles that make is aware of.

Running a simple make at the root of an example repository returns output similar to the following:

Available targets: help Display this help text. test Run the test suite Any new arrivals to the codebase can proceed to get started quickly with pre-written scripts or make targets that they learn from make help.

Another helpful approach is to wrap other tasks within sandboxed environments so that running them becomes portable and alleviates the need to set up local installations for command-line tools. As an added benefit, it offers a similar execution environment inside CI/CD.

For example, consider the previous section’s use of the editorconfig-checker command. If you’d like to perform this check within CI/CD, installation is a necessary prerequisite to running the command. However, you may alternatively wrap the check within a docker command which provides a common installation path and consistent behavior. This Makefile target runs a docker command that mounts the repository into the container and performs EditorConfig checks.

.PHONY: editorconfigeditorconfig: ## Perform editorconfig checks@docker run --rm --volume $$PWD:/check mstruebing/editorconfig-checker Performing EditorConfig tests is a matter of running:

make editorconfig Similarly, you can wrap the call to vale within a container as well. This Makefile target will perform style checks for files in the docs/ directory:

.PHONY: valevale: ## Run vale style checks@docker run --rm -v $$PWD/styles:/styles \ --rm -v $$PWD/:/docs \ -w /docs jdkato/vale docs Wrapping helper scripts inside of docker means that, instead of a long list of repository prerequisites such as editorconfig-checker, vale, etc., users just need a functional installation of Docker to run an assortment of different commands. Additionally, the double-commented documentation string beginning with ## will display all shortcuts when using make help to make them easy to find.

General guidance for specific languagesWe’ve taken a look at three different approaches that are useful for nearly any type of language repository, but some languages offer common strategies that may be useful if you and your team happen to be using them:

  • Commands like terraform and go provide an fmt subcommand that unifies code formatting. Whenever possible, enforcing formatting standards with jobs that call commands like go fmt can greatly reduce churn in codebases due to changes in style or other inconsistencies.
  • Some languages like Python natively support sandbox mechanisms like venv which you can use in place of Docker.
  • If your team uses one editor consistently, consider committing to a common repository editor configuration. For example, the .vscode directory can store common settings for a specific codebase. Even venerable editors like Emacs support this practice with the.dir-locals.el file.

ConclusionWe hope that this article has been a helpful exploration of some of the more generally applicable types of CI/CD jobs that you might use in your projects. When choosing which shortcuts and scripts to integrate into your repository, remember the compounding effects of automation: when you share ways to save engineering time, everybody benefits — often repeatedly, particularly when automation gets used in CI/CD environments.

The post 3 CI/CD Jobs for Any Repository appeared first on Semaphore.

View Details

Putting these tips into practice will help your team use its time efficiently and communicate in a way that benefits the whole team.

The post 6 Tips for Improving Dev Team Communication and Collaboration appeared first on Semaphore.

View Details

Software Architect and author Vaughn Vernon helps us understand the nuances of domain-driven design.

The post Vaughn Vernon on Domain-Driven Design for Software Architecture appeared first on Semaphore.

View Details

React Native has kept up well in delivering best-in-class experiences for small, medium, and large-scale applications. Let's have look at what landed in React Native this year and what we can expect in the foreseeable future.

The post React Native in 2022 and Beyond appeared first on Semaphore.

View Details

The complexity of Kubernetes makes it remarkably easy to leave a cluster vulnerable to attacks. Use Kubescape to prevent vulnerabilities from reaching your cluster.

The post Secure Kubernetes with Kubescape appeared first on Semaphore.

View Details

Is this the job of your dreams or nightmares? Sometimes it's best to keep shopping around. Ask these questions after the interview to find out.

The post 36 Questions to Ask Your Future Software Employer appeared first on Semaphore.

View Details

Tauri co-founder Daniel Thompson-Yvetot shows us how developers can use Tauri to build better apps.

The post Daniel Thompson-Yvetot on How to Build Hybrid Apps With Tauri appeared first on Semaphore.

View Details

Learn what an API layer is, why your backend needs one, and how to equip your Node.js backend with an API layer.

The post Why Your Backend in Node.JS Needs an API Layer and How to Build It appeared first on Semaphore.

View Details

CI/CD is grounded in the philosophy that we can deliver quality features faster. Learn how to set up a CI/CD Pipeline With FastAPI, AWS, and Semaphore.

The post How to Set Up a CI/CD Pipeline With FastAPI, AWS, and Semaphore appeared first on Semaphore.

View Details

Learn how to improve your code security and dependency checking by getting warnings about attack vectors in an automated and actionable way.

The post Elixir Code Security: Prioritize Security in Your CI With 4 Tools appeared first on Semaphore.

View Details

Ken Kantzer shares with us his views on management, web development security, and code auditing.

The post Ken Kantzer on Secure Development With Code Audits appeared first on Semaphore.

View Details

Systems are subtle and adapting them to solve specific problems takes time and experience. There is rarely a one-size-fits all solution. Elixir has offered a refreshing alternative to other options, and in the case of large-scale data scraping, it punches far above its weight.

The post Efficiency With Elixir appeared first on Semaphore.

View Details

Starting from December 1, 2022, Semaphore will be removing the Xcode 12 image from the list of available environment variables.

The post Deprecating Xcode 12 in Semaphore appeared first on Semaphore.

View Details

DevOps metrics are the vital signs of your project. The metrics you choose can expose problems or can hide them behind irrelevant data.

The post 17 DevOps Metrics To Measure Success appeared first on Semaphore.

View Details

Learn about the 'brain spans' concept and discover the guidelines to write code that is easy to read and flows like a story.

The post The Secret Art of Storytelling in Programming appeared first on Semaphore.

View Details

Meet Project Insights - your go-to instrument for monitoring your CI/CD reliability, stability and throughput.

The post Measure Your Team’s CI/CD Performance with Project Insights, Semaphore’s Newest Feature appeared first on Semaphore.

View Details

Tech Ladies Vice President Caro Griffin teaches us the value of sustainable communities and encourages companies to diversify their teams to grow their business.

The post Caro Griffin on Recruiting Talent From Communities appeared first on Semaphore.

View Details

Learn how to set up Semantic Release and Fastlane for releasing React Native apps on Semaphore efficiently.

The post Automate Your React Native Release Process With Semantic Release, Fastlane, and Semaphore appeared first on Semaphore.

View Details

We would like to inform all Apple developers that Xcode14 is now available as a CI/CD environment on Semaphore.

The post Xcode 14 Is Now Available in macOS Monterey Image appeared first on Semaphore.

View Details

Learn how to add Rubocop to your Rails applications today for incremental improvements on the style of your code.

The post How to Use Rubocop for Your Team’s Rails Apps appeared first on Semaphore.

View Details

Starting from October 15, 2022, Semaphore will be rounding up smaller jobs.

The post Billing Update in Semaphore: Rounding up Smaller Jobs to 1 Minute appeared first on Semaphore.

View Details

This article will take you through a few preventative steps to help you keep your CI/CD pipeline secure.

The post Improving Security in Your CI/CD Pipeline appeared first on Semaphore.

View Details

Allen Holub discusses with us his idea of #NoEstimates and shares his views on what software development should look like.

The post Allen Holub on Why You Should Get Rid of Estimates appeared first on Semaphore.

View Details

Can we take microservice architecture patterns and apply them to the frontend?

The post Microfrontends: Microservices for the Frontend appeared first on Semaphore.

View Details

Don’t waste your DBA talents, automate database management with continuous integration.

The post Database Management With CI/CD appeared first on Semaphore.

View Details

Unlike CRA, Vite does not build your entire application before serving, instead, it builds the application on demand. It also leverages the power of native ES modules, esbuild, and Rollup to improve development and build time.

The post 4 Reasons Why You Should Prefer Vite Over Create-React-App (CRA) appeared first on Semaphore.

View Details

Audit logs allow you to collect such information as creation or removal of secrets, starting or stopping a job, creating/removing a workflow, modifying a project, and much more.

The post Introducing Audit Logs for Your Organization’s Security and Compliance appeared first on Semaphore.

View Details

Software developer and consultant Daniel North gives his thoughts on common testing errors and how to simplify policy.

The post Daniel Terhorst-North on Simplifying Testing and Development appeared first on Semaphore.

View Details

Is your monolith on its last legs? Are you having trouble meeting the rising demands of your users? Then, check out our free ebook on migrating from monolith to microservices.

The post New book: Transitioning from Monolith to Microservices Handbook appeared first on Semaphore.

View Details

Elite teams are thousands of times more productive than the rest. DORA metrics can help your team ship code at a faster pace.

The post Become an Elite Team With Dora Metrics appeared first on Semaphore.

View Details

This article explains how the test types available on Rails and Rspec relate to the different levels of the pyramid, helping you determine how many of each type to write.

The post Applying the Test Pyramid Concept to Ruby on Rails Apps appeared first on Semaphore.

View Details

Now, you can restrict access to secrets only to organization admins and define which projects can use a secret in Semaphore.

The post Enhancing Security of Secrets: Project Whitelists and Admin-only Edit Permissions appeared first on Semaphore.

View Details

Learn the benefits and constraints of microservices, microservice migration, and why to choose to build microservice applications from scratch instead of modernizing monolithic applications.

The post Sarah Wells on Mastering Microservices Over Monolithic Applications appeared first on Semaphore.

View Details

With pre-flight checks, admins can set up custom checks that the system needs to pass before any pipeline can start running. Learn more about this new feature.

The post Pre-flight Checks: Set up Custom Security Checks for Pipeline Execution appeared first on Semaphore.

View Details

As software creators, we are responsible for accurately communicating the compatibility for our releases.

The post Semantic Versioning with CI/CD appeared first on Semaphore.

View Details

Simplify how you layout systems with event modeling and learn the benefits of describing and storing system information.

The post Adam Dymitruk on How to Upgrade Your Toolbox with Event Modeling appeared first on Semaphore.

View Details

Learn what an API layer is, some of the many benefits it can provide to your architecture, and how to implement it in React.

The post Why You Need an API Layer and How To Built It in React appeared first on Semaphore.

View Details

Bun is the newest and fastest all-in-one JavaScript framework. Will it take over Node? Is it better than Deno? Let's do some testing.

The post JavaScript’s Newest Framework: Bun. Will It Take Node’s Crown? appeared first on Semaphore.

View Details

In software development, it seems self-evident that bugs are bound to happen. Still, according to Developer and Agile Consultant Arlo Belshee, they don’t have to.

The post Arlo Belshee on How to Scale Software Development Teams appeared first on Semaphore.

View Details

Terraform is one of the most popular IaC tools available. Learn its benefits and how to use it.

The post A Developer’s Guide to Terraform appeared first on Semaphore.

View Details

Restructuring your organization to support microservice-based applications requires an entire culture shift and new way of working.

The post How to Restructure Your Organization for Microservice Applications appeared first on Semaphore.

View Details

Migrating from a monolith to microservices is also not a simple task, and creating an untested product as a new microservice is even more complicated. Microservices should only be seriously considered after evaluating the alternative paths.

The post When Microservices Are a Bad Idea appeared first on Semaphore.

View Details

Engineers and testers need to work together to ensure quality products. Find out how coding and testing relate to each other despite their points of conflict and how to achieve collaboration between them.

The post Janet Gregory on Holistic Testing: Where Testing and Coding Meet appeared first on Semaphore.

View Details

Microservices, by their networked nature, provide more points of failure than a traditional monolith. As a result of this, we need a different, broader approach to testing.

The post Testing Strategies For Microservices appeared first on Semaphore.

View Details

From VMs to Kubernetes and Serverless, there are many ways of to deploy microservices. The perfect method is determined by size and scaling requirements.

The post 5 Options for Deploying Microservices appeared first on Semaphore.

View Details

Senior Principal Software Engineer Holly Cummins teaches us about microservice architecture and how to test it.

The post Holly Cummins on Getting Into and Testing Microservices appeared first on Semaphore.

View Details

Like tidying up a house before a total renovation, preparing your monolith is the first step towards transitioning to microservices.

The post 12 Ways to Improve Your Monolith Before Transitioning to Microservices appeared first on Semaphore.

View Details

Releasing microservices applications is hard when code is scattered among dozens of repos. Here's why using a monorepo might make more sense.

The post Release Management for Microservices appeared first on Semaphore.

View Details

Semaphore now allows users to integrate their Bitbucket accounts and run builds on any changes in their Bitbucket repositories.

The post Introducing Bitbucket integration in Semaphore appeared first on Semaphore.

View Details

In this podcast episode, we pick up the perks mob programming offers to developer teams and how it gives green light to dynamic exchanges between junior and senior team members.

The post Woody Zuill on How Mob Programming Makes the Difference appeared first on Semaphore.

View Details

Learn how to build and deploy a face-recognition application using serverless functions in AWS. Along the way, we’ll discover the Serverless Application Model, how to test Lambdas, and optimize monorepo CI/CDs.

The post AWS Serverless With Monorepos appeared first on Semaphore.

View Details

Semaphore’s Test Reports help your team to get an effective view of your CI/CD test suite. Here's what's new in Test Reports.

The post What’s New in Test Reports appeared first on Semaphore.

View Details

Build scalable microservices applications that are aligned with your business needs with Domain-Driven Design.

The post Domain-Driven Design Principles for Microservices appeared first on Semaphore.

View Details

Microservice architecture is the most scalable way of developing software. But if you implement it wrong, you won't enjoy its benefits.

The post What Is Microservice Architecture? appeared first on Semaphore.

View Details

Agile coach and creator of ApprovalTests Llewellyn Falco tells us about the benefits mob programming has for developing, testing, and business.

The post Llewellyn Falco on Mob Programming for Optimizing Testing and Team Growth appeared first on Semaphore.

View Details

Learn how E2E testing can be as effective as other types of testing to ensure the performance & reliability of the system for end-users.

The post Why End-to-End (E2E) Testing is Often Good Enough appeared first on Semaphore.

View Details

Learn how to deploy a Go web application with Docker, and how Docker can help improve your development workflow and deployment process.

The post How To Deploy a Go Web Application with Docker appeared first on Semaphore.

View Details

Artifacts are now generally available in Semaphore. With artifacts, you can store data on your projects and pass it between jobs, workflows, and projects. Read more.

The post Semaphore Artifacts: a Fast and Secure Way to Store Data on Your Projects appeared first on Semaphore.

View Details

Get familiar with Gin and find out how it can help you reduce boilerplate code and build a request handling pipeline.

The post Building Go Microservice with Gin and CI/CD appeared first on Semaphore.

View Details

Is your build up to par? If you need help speeding it up, here are five ways to run your CI/CD build faster.

The post 5 Ways to Run Faster CI/CD Builds appeared first on Semaphore.

View Details

We ask Gaspar Nagy, the person behind SpecFlow, about his experience with BDD and testing, their challenges, and how they can make a difference in business.

The post Gaspar Nagy on How to Improve Development and Business with BDD Testing appeared first on Semaphore.

View Details

Find out how to implement a CI/CD pipeline for a microservice running in Docker with Semaphore and DigitalOcean Kubernetes.

The post CI/CD for Microservices on DigitalOcean Kubernetes appeared first on Semaphore.

View Details

In this tutorial, you'll learn how to build and test a Docker image using Continuous Integration. Then, we'll extend CI/CD with Continuous Deployment to a Kubernetes cluster.

The post CI/CD for Microservice Spring Boot appeared first on Semaphore.

View Details

Learn how to leverage Docker’s advantages to easily develop and deploy a PHP application to Heroku, using Semaphore for continuous deployment.

The post Dockerizing a PHP Application appeared first on Semaphore.

View Details

In this episode, Darko Fabijan welcomes Kent C. Dodds, Director of Developer Experience at Remix. Kent explains what Remix is and why it’s so cool. Listen now!

The post Kent C. Dodds on How to Build Better Websites with Remix appeared first on Semaphore.

View Details

Get an understanding of how to dockerize your Django application, using the Gunicorn web server, capable of serving thousands of requests in a minute.

The post Dockerizing a Python Django Web Application appeared first on Semaphore.

View Details

Are you ready to ace you software interview? We’ve collected and answered 30 common CI/CD interview questions. How many can you answer?

The post 30 Common CI/CD Interview Questions (with Answers) appeared first on Semaphore.

View Details

Learn how to build and dockerize a Ruby on Rails application that uses PostgreSQL, Redis and Sidekiq.

The post Dockerizing a Ruby on Rails Application appeared first on Semaphore.

View Details

Docker has significantly improved the way we build, ship and run apps. Read this tutorial to learn how to integrate Docker with your Node.js application.

The post Dockerizing a Node.js Web Application appeared first on Semaphore.

View Details

TestFlight makes it easy for developers to publish apps to early or beta testers and Semaphore is a fast CI/CD service that supports iOS deployment to TestFlight.

The post Automate Flutter App Deployment on iOS to TestFlight using Fastlane and Semaphore appeared first on Semaphore.

View Details

We’re proud to announce that Semaphore has been named as a High Performer among Test Automation and Continuous Integration tools on G2.

The post Customers Name Semaphore as Top Performing CI/CD Tool appeared first on Semaphore.

View Details

In this podcast episode, Darko and Shai talk about Shai’s career path from being a full-time developer to growing his startup to consultancy, why writing tests can save developers’ time, and how to write tests the test effective way.

The post Shai Reznik: How to Write Tests the Most Test Effective Way appeared first on Semaphore.

View Details

How do you write a blog that other developers will enjoy? Check out our 21 tips to write better posts for developers.

The post 21 Tips to Write Better Posts for Developers appeared first on Semaphore.

View Details

We have released a new guide containing every piece of wisdom we could muster to help you write fast tests and keep them fast. The guide is open source and you can download it today.

The post A Complete Guide to Optimizing Slow Tests appeared first on Semaphore.

View Details

For a company like Semaphore that consists of small and agile teams where every individual counts, finding the right engineers is crucial to success.

The post Interviewing Engineers at Semaphore appeared first on Semaphore.

View Details

In the latest episode of Semaphore Uncut, we sit down with Maryam Umar, Head of QA at Rezolve, to talk about careers in test automation and growing engineering teams. Listen now.

The post Maryam Umar: How to Advance Your Career and Grow the Team in Test Automation appeared first on Semaphore.

View Details

Now, you can host your CI/CD behind your company firewall, fully on your own servers or virtual machines. Contact us to learn more.

The post Introducing Semaphore On-Premise: Host Your CI/CD behind Your Company Firewall appeared first on Semaphore.

View Details

We're introducing premium support plans to Semaphore! Our customer support team will walk an extra mile to ensure your CI/CD goals are met. Read more.

The post Premium Support Packages in Semaphore appeared first on Semaphore.

View Details

There's so much more to testing than just writing automated tests that run in CI. In this podcast episode, Lisa Crispin, Author, talks about the holistic approach to testing. Listen now.

The post Lisa Crispin: Holistic Approach to Testing appeared first on Semaphore.

View Details

Behavior-Driven Development (BDD) is about minimizing the feedback loop. It is a logical step forward in the evolution of software development practices. This article explains the concept and its origins.

Waterfall If you are a software developer or an engineering manager, you are probably familiar with the Waterfall Model, shown in the following diagram:

What was later named “Waterfall” was first formally described by Winston Royce in his 1970 paper “Managing the development of large software systems”. I recommend reading the whole paper to understand the idea. Most people learn about it secondhand and assume that this process was presented as the ultimate solution at the time. However, Royce recognized that having a testing phase at the end of the development process was a major problem:

“I believe in this concept, but the implementation described above is risky and invites failure. […] The testing phase, which occurs at the end of the development cycle, is the first event for which timing, storage, input/output transfers, etc., are experienced as distinguished from analyzed. […] The required design changes are likely to be so disruptive that the software requirements upon which the design is based and which provides the rationale for everything are violated. Either the requirements must be modified, or a substantial change in the design is required. In effect, the development process has returned to the origin, and one can expect up to a 100-percent overrun in schedule and/or costs.”

This model is still used to develop software in many companies worldwide for various reasons. Waterfall implies flow, but in practice, there are always feedback loops between phases. All major improvements to the model over time have been made by minimizing the feedback loops and making them as predictable as possible.

For example, if we write a program, we want to know how long it will take us to find out if it works. On the other hand, if we design a part of a system, we want to learn if it is actually programmable and verifiable, and at what cost.

So, when we look at a feedback loop, we look for methods we can use to minimize it. At first, our goal is to remove obviously wasteful work. Later, we realize that we can optimize and do things faster and better than we could have ever imagined back when we were doing things the old way.

The first optimization: Test-First Programming The first optimization emerged from the Coding and Testing phases. In the traditional quality assurance-based (QA-based) development model, a programmer wrote code and submitted it to the QA team. It took a day, a few, or weeks to get a report on whether the code worked and if the rest of the program worked as well. There were often bugs, so we would have to go back to programming and fix any issues, even though we thought the work was finished.

To cut down the feedback loop, developers started coding and verifying simultaneously, i.e. writing some code, and then writing some tests for it. Tests produced an excellent side effect–the automated test suite–which we can run at any time to verify every part of the system for which we have written a test. From this emerged the desire to have a test suite that covers the entire system in order to be able to work as safely as possible.

The feedback loop of coding followed by testing still takes some time, so the next step was to invert it: writing tests before writing a single line of code. In this way, the feedback loop shrinks, and it doesn’t take long to realize that developers are writing only the code needed in order to pass the existing tests.

This is called Test-First Programming. When working test-first, tests are used to help “fill in” the implementation correctly. This reduces the number of bugs, increases programmer productivity, and positively affects the tempo of the whole team.

The problem with writing tests later The Waterfall Model puts tests at the end of the development cycle, which, as we’ve seen, is very inefficient. But that is not the only problem. Code written in isolation is difficult to test because developers are focused on solving a problem rather than writing testable code.

Later, when we begin writing tests, the mindset is yet again in the wrong place: we’re concentrating on testing that the code we wrote is indeed the code we wrote, instead of testing the code’s behavior.

The result of this is that code and tests become highly coupled. This results in even more problems when we refactor. Changing tightly-coupled code makes the related test obsolete — we have to rewrite a bunch of tests every time we change something in the code. The feedback loop suffers and gets longer as we continually deal with the consequences of past decisions.

The solution is to have a test-first mindset. When tests come first, no pre-existing code influences how we write them and we can write tests that check what the code is actually supposed to do. We also get another benefit: the resulting code is mode modular and testable, making tests smaller and more readable.

Test-Driven Development Once we have a continuous loop of testing and coding, we’re still doing all our program design upfront. We’re using Test-First Programming to make sure that our code works, but there’s a feedback loop where we may find out (disturbingly late) that a design is difficult to test, impossible to code, performs poorly, or just doesn’t fit together with the rest of the system as we are trying to implement it.

To minimize this loop, we apply the same technique. We invert it again by doing Test-First Programming before we start designing. Or rather, we do the Testing, Coding, and Program Design steps all at the same time. A test influences code, which in turn influences design, which influences our next test.

It quickly becomes clear that this cycle organically drives design ideas, and we start to implement only the parts of the design that we need in a way that can easily evolve. Design now includes a substantial refactoring step, which gives us the confidence to under-design instead of over-engineer. That is, we end up with just enough design and appropriate code which meets our current requirements.

This is Test-Driven Development (TDD). It combines Test-First Programming with design thinking by continuously applying refactoring principles and patterns. The positive side-effects are now amplified: we have not only reduced the number of bugs, but we are also not writing any code that doesn’t help us implement a feature. This further increases the team’s productivity by helping avoid design mistakes which are more costly to fix down the road.

TDD is the crystallization of an old idea that says that design and testing should be interlaced in a continuous iteration loop:

“A software system can best be designed if the testing is interlaced with the designing, instead of being used after the design. [..] A simulation which matches the requirements contains the control which organizes the design of the system. [..] Through successive repetitions of this process of interlaced testing and design, the model ultimately becomes the software system itself.”

— First NATO Software Engineering Conference (1968)

Making the next step with Behavior-Driven Development Now that we are designing, coding, and testing in one loop, it’s time to revisit at the Analysis step. By analysis, I assume “understanding what we need to build”. Again, we’re interested in optimizing the loop in the name of efficiency. In practice, this would involve preparing a list of a dozen or more features and passing it on to developers, who would complete them all before moving forward. This way, we often end up implementing features that we don’t need. Sometimes we also discover new features we didn’t expect or discover something new about the features that we know we need.

We can apply the same technique and bring Analysis into our loop. Now we test-drive a feature before we try to implement another. It is worth illustrating that the duration of such cycles for a developer is measured in hours, sometimes even minutes, not in days or weeks.

After applying this technique consistently for a while, we notice that we tend to break down all features in the smallest units and consistently deliver them one by one. Our understanding of how features affect one another improves and we find ourselves able to respond to changes faster. This allows us to identify and discard unwanted features quickly and prioritize important features.

By test-driving our analysis, we better understand the system’s behavior and how to appropriately design and implement it. At the same time, all that we are doing from day one is producing a test suite, which keeps our entire system constantly verifiable.

This is called Behavior-Driven Development (BDD). It saves time for both the stakeholders (business owners) and the development team. By asking questions early, developers help both themselves and the stakeholders gain a deep understanding of what they are building. Stakeholders get results at a predictable pace, and since the features are worked on in small chunks, estimates can be done more accurately and new features can be planned and prioritized accordingly.

BDD: what, not how To practice BDD, we must first write a specification that describes the behavior of the system. We do this by asking questions such as: “how should the system respond when a user does X?” or “does X follow the user’s expectations?” This puts the focus on the problem we are working on without getting mired in implementation details. We think about what the system does, rather than how it does it.

We really don’t care how the system looks at this stage, if it has round or square buttons, or even what devices it runs on. Thus, we decouple the problem we want to solve from the technical details. The specification presents the case in a clear and concise form, as shown in the example below:

``` Scenario: Blog Search Given I visit the blog page When I search for “BDD” Then I get posts related to BDD

```

The example is written in Gherkin, a language used by frameworks like Cucumber to define test cases in the BDD-style. Under the hood, the scenario drives a series of automated tests that ensure that the specification is followed.

BDD allows us to interweave tests, analysis, and design, so they feed into each other, creating a feedback loop that guides development and enables us to track our progress.

BDD is not UI testing Inevitably, as an idea becomes popular, some of the important nuances get lost. One of the biggest misconceptions about BDD is that it is a synonym for UI testing.

Take this example:

Scenario: user logs in to application Given authorized user “John” When I enter “John” in the username field And I enter “sekret1” in the password field And I click the login button Then the homepage should open

This Gherkin scenario has several problems. For one thing, it’s too focused on the UI, making it brittle and too tied to the implementation — if any of the field names change, the test breaks.

Another problem is that the logic of the test is limiting because it forces developers to follow a recipe. They need to implement classic username/password authentication even when better authentication mechanisms such as fingerprints or face recognition are available. Scenarios that are too detailed make the lives of developers harder because they hide the problem to solve under a long list of steps to follow.

A better version of the scenario is shown below. In the example, we establish the specification (what the system should do) without going into details (how it should do it). Developers are free from the shackles of following a predetermined solution and are free to innovate.

Scenario: user logs into application Given authorized user “John” When “John” logs in correctly Then “John” can access their items

BDD works at every level Another big misconception about BDD is that it does not work for integration and unit testing. The reason for this is that Gherkin introduced an additional layer of abstraction which has led people to believe that BDD is synonymous with end-to-end testing. While BDD is usually paired with acceptance tests, nothing is preventing us from using it at any level of the testing pyramid.

To see how that BDD can be used at every level, here’s a unit test written in Gherkin:

``` Feature: Sum a Pair It sums a pair of numbers

Scenario: adding numbers Given a 1 When add a 2 Then the sum is 3 ```

The example is so simple that you could even throw away Gherkin and write the test directly in any framework supporting behavior-driven DSLs such as Jest, as shown here:

``` import { sum } from './maths';

test('adds 1 + 2 to equal 3', () => { expect(sum(1, 2)).toBe(3); }); ```

Thinking that BDD only works on high-level tests leads to the inverted test pyramid (also known as the ice-cream cone). While in some cases, the inverted pyramid is an acceptable solution, more often, it only leads to hard-to-maintain, complex test suites that run very slowly.

Going even faster If you look at the remaining phases listed in the Waterfall diagram, which all need to happen regardless of the methodology, you may wonder if the same feedback loop minimization can be applied to them. The answer is, of course, yes. However, such loops are of a scope that is broader than just design and development, and they involve people working across very different fields, which means that they are out of the scope of this article. However, I will mention them briefly.

Lean Startup would be the closest concept that brings together requirement gathering, feature development, and marketing as a way to close the loop on learning what a startup needs to build. Of course, the process goes somewhat differently in enterprises, although they are learning to apply lean startup principles in many projects as well.

Merging BDD with deployment and operations brings us to the broad concept of continuous delivery. The most important processes are continuous integration (CI) and continuous deployment, which you can easily configure for any project on Semaphore.

Conclusion Behavior-Driven Development evolved from optimization of various phases in the software development process. We can produce better software by analyzing, testing, coding, and designing our system in one short feedback loop, which helps us avoid mistakes and wasteful work.

It is a common misconception that TDD is about testing and that BDD is just another way of approaching software testing, since it has its origins in TDD. This is not the case, although tests are a nice byproduct. It is a holistic approach to software development, derived from one simple idea: the desire to optimize the feedback loops in our work.

BDD is a powerful tool for effective software development, one that takes time to learn and apply well. It helps you write tests that add value, have meaning, and help you in your designs. As long as you stay focused on the ‘what’ and avoid the ‘how’, you’ll be fine.

The post Behavior-Driven Development appeared first on Semaphore.

View Details

For over a decade, the testing pyramid has been helping developers plan automated software tests. In this article, we’ll explore what makes up the pyramid, how it is helpful, and what alternative “shapes” there are.

What is the Testing Pyramid? Introduced by Mike Cohn in his book Succeeding with Agile (2009), the pyramid is a metaphor for thinking about testing in software. It’s an idea that has caught on so strongly that, to this day, it’s still the industry standard in engineering circles.

The pyramid attempts to visually represent a logical organization of testing standards. It consists of three distinct layers:

The base of the pyramid consists of unit tests. A unit is a small logical piece of code: it can be a function, a class, or even a method in a class. A unit test only checks that said unit behaves as the developer intended. By calling the tested code directly and evaluating its output, a developer can write a unit test without depending on any other components, services, or the UI.

One level above, at the middle of the pyramid, we find integration tests, which are called “service tests” in Mike’s book. Integration in this context refers to testing how different components of the system work together. For instance, if a model in the code can correctly exchange data with the database or if a method can retrieve information from an API. No UI interactions are needed, as integration tests can directly call the code at the interfaces.

At the top of the pyramid we find the end-to-end tests (E2E). Also known as UI tests, E2E is testing in its most intuitive sense: use the application and see if it works. But instead of having a human conducting the tests, E2E tests are entirely automated. Every user interaction is mimicked; an E2E test can click buttons, type values, and evaluate what the UI is showing.

As you can see, the three types of tests have very different scopes:

  1. Unit tests can only find logical errors at the most fundamental level. They are fast and require very few resources to run.
  2. Integration tests verify that services and databases work well together with the code and the classes you’ve written. They can only find problems at the interfaces where two or more components meet.
  3. E2E tests depend on the complete application being able to start. These are the most comprehensive type of tests we have and, accordingly, need the most computing resources and time to run.

So, why a pyramid? To understand how the pyramid got its shape, we must understand the intricacies of each type of test.

Unit tests are small and therefore easy to write and maintain. Because they test very narrow parts of the code, we need plenty of them. This is usually not a problem because unit tests are light enough that we can run thousands of them in a few seconds.

E2E tests are at the far end of the spectrum. They are complex to write, difficult to maintain, need plenty of resources, and are slow to run. But, since we can cover a lot of the application with a few E2E tests, we need fewer of them.

In the middle, we find integration tests. Complexity-wise, they are on the same page as unit tests. But we don’t need as many of them since we are only interested in testing the “edges” of the application. Compared with unit tests, integration tests need more resources to run but are the same order of magnitude.

Hopefully, you now understand why the pyramid has its shape: the width of each layer represents the ideal relative quantity for each kind of test. In other words, the pyramid says we must have a few end-to-end tests, a decent amount of integration tests, and a swarm of unit tests.

As you work up the pyramid, tests get more complex and cover a more significant portion of the codebase. At the same time, the effort of writing, running and maintaining them increases. The pyramid illustrates an ideal ratio that maximizes the chance of finding a bug with the least work.

The forces shaping the pyramid The nature of software development can often make the pyramid appear spontaneously — even when developers didn’t consciously set out to do it. Why does this happen?

It’s challenging to write E2E tests when the project is just starting. Unless the development team adopts a framework such as BDD and sets out to write acceptance tests from the beginning, most E2E tests will be written only when a basic prototype or a minimum viable product is in place. By then, developers will have had plenty of opportunities to write unit and integration tests.

A second factor that shapes the pyramid is speed. The faster the test suite is, the more often developers run it. Slow tests hurt the vital feedback loop needed for a productive environment.

Tests at the bottom of the pyramid are the fastest. So developers tend to write more of them. Conversely, E2E tests are slow and thus used more sparingly. As a result, a large web app can have thousands of unit tests, hundreds of integration tests, and a few dozen E2E tests.

| Test Type | Order of magnitude | | --- | --- | | Unit test | 0.01 – 0.001 s | | Integration test | 1 s | | E2E test | 10 s |

The testing pyramid is the most widely-known format of designing automated tests. But is it the only one? Certainly not.

Testing Frontends with the Testing Trophy The testing pyramid dates back to 2009. To put things into context, Ruby on Rails was on its second release and Node.js was only being created. Internet Explorer and Adobe Flash were still relevant. MySpace had just peaked and Facebook was only getting started.

Rich frontend frameworks like React or Angular were still far away on the horizon.

Technology has changed so much that many people feel that a different approach is needed. Kent C. Dodds is one such person and proposed the Testing Trophy as an alternative way for structuring tests in frontend development.

Caption: “Write tests, not too many, mostly integration.” Credit: Kent C. Dodds at testingjavascript.com

The Testing Trophy reorders priorities. Integration tests are king as most modern UIs rely on backend components and are difficult to test in isolation.

Compared to the pyramid, unit tests take a back seat and are replaced by static testing tools such as ESLint and JSHInt. These scan the code to offer suggestions and find potential problems such as use of unsafe statements or lack of adherence to variable naming rules.

The trophy is crowned by E2E tests, which take a similar portion of the testing pie as in the pyramid.

The Test Matrix One thing that’s often left out of the equation when discussing the pyramid is confidence. Which type of test gives you more confidence? The only test that can genuinely validate the application’s usability is an E2E test.

What’s stopping us from writing a lot more E2E tests? The typical answer is effort: running E2E tests is rarely worth the time and maintenance effort. But not everyone agrees with this argument. Gleb Bahmutov and Roman Sandler proposed the Testing Matrix as an alternative device for planning a testing strategy.

In the matrix, effort increases from left to right and confidence rises from the bottom to the top. The best place to be is the green quadrant.

Most software projects start in the low-effort, low-confidence yellow zone.

Tests are added at every level as the project matures and new features are added. The upkeep of the test suite grows entropically as one or more of the effort categories increase. A team that neglects to upkeep its test suite may soon find itself in the red zone.

How can you increase confidence and reduce effort? The answer is to periodically reevaluate the characteristics of your tests in the following five categories:

  • Installation: the effort involved in installing and setting up the test framework.
  • Writing: the complexity of writing tests and the skill level of the developers for a given framework.
  • Running: the difficulty of running the test suite and CI/CD performance.
  • Debugging: how easy it is to find and fix a problem when a test fails.
  • Maintenance: how much effort is required to maintain a test throughout the project’s lifetime.

Unit tests may be the best investment at the start of the project. But once features have stabilized, you may need to rebalance the mix by adding more E2E tests and removing some in other categories. This should increase confidence while reducing, or at least maintaining, effort level.

Beware of dogmas The pyramid tells us to limit E2E tests due to speed, cost, and maintenance concerns. But this is not true in every situation. As Gleb Bahmutov remarks in this Semaphore Uncut Episode, we can imagine scenarios where E2E tests are easy to maintain:

“As for end-to-end tests, they operate like a user. By definition, you test through the public interface of your website. If you change implementation under the hood, you can swap your whole backend. The test should not be concerned. The maintenance should actually be much lower.”

— Gleb Bahmutov, Semaphore Uncut

Every team, every project, every organization is different. As requirements change, a team may decide to respec the suite. Having the flexibility to stop and reevaluate the cost-benefit equation, and adjust as needed, is critical to reaching a low-effort and high confidence zone.

The role of CI/CD in your testing suite All the “shapes” discussed in this post are valuable models. But none of them should be blindly followed.

As the testing suite grows and is rebalanced, CI/CD pipelines must also adapt. While the project is still young, you can maintain the process in the low-effort quadrant by running tests in sequence and putting fundamental jobs at the beginning of the pipeline. For instance, if you have mostly unit tests in your suite, running them at the beginning will help you fail fast.

Later, when the CI pipeline begins to struggle under the burden of multiple integration and E2E tests, you will find that unless you start to parallelize some workloads, your pipeline might become too slow.

As your CI/CD approaches the critical 10-minute mark, you will have to reorganize your pipelines and optimize any slow tests you have to keep the vital feedback loop fast and nimble.

Final thoughts The testing pyramid model gives such benefits that it has survived for more than a decade. It introduced the healthy habit of thinking about testing and has set a common language across the industry.

Yet, the pyramid is not as fresh as it used to be. New practices, technologies, and cultural changes mean that the pyramid makes less sense than before. Cracks have begun to show. As a result, alternative models have appeared and will continue to appear.

In the end, you have to decide which approach is best for your project. What does your test suite look like? Thanks for reading.

The post The Testing Pyramid: How to Structure Your Test Suite appeared first on Semaphore.

View Details

What is a software product? The business code itself, right? Actually, that’s only a part of it. A software product consists of different elements:

  1. Business code
  2. Documentation
  3. CI/CD pipeline
  4. Communication rules
  5. Automation tests

Pure code is not enough anymore. Only if these parts are integrated into a solid system, can it be called a software product.

Tests are crucial in software development. Moreover, there is no separation between “application” and “tests” anymore. Not because the absence of the latter will result in an unmaintainable and non-functional product (though it’s definitely the case), rather due to the fact that tests guide architecture design and assure code testability.

Unit tests are only a fraction of the huge testing philosophy. There are dozens of different kinds of tests. Tests are the foundation of development these days. You can read my article about integration tests on Semaphore’s blog. But now it is time to deep dive into unit testing.

The code examples for this article are in Java, but the given rules are common for any programming language.

Table of contents 1. What is unit testing 2. Test-driven development 3. Unit tests requirements 4. The unit testing mindset 5. Best practices 6. Unit testing tools

What is unit testing Every developer has experience in writing unit tests. We all know their purpose and what they look like. It can be hard, however, to give a strict definition for a unit test. The problem lies within the understanding of what a unit is. Let’s try to clarify that first.

A unit is an isolated piece of functionality.

Sounds reasonable. According to this definition, every unit test in the suite should cover a single unit.

Take a look at the schema below. The application consists of many modules, and each module has a number of units.

In this case, there are 6 units:

  1. UserService
  2. RoleService
  3. PostService
  4. CommentService
  5. UserRepo
  6. RoleRepo

According to the given schema, the unit can be defined in this way:

A unit is a class that can be tested in isolation from the whole system.

So, we can write tests for each specific unit, right? Well, this statement is both correct and incorrect, because units do not exist independently of one another. They have to interact with each other, or the application won’t work.

Then how can we write unit tests for something that cannot practically be isolated? We’ll get to this soon, but let’s make another point clear first.

Test Driven Development Test Driven Development is the technical practice of writing tests before the business code. When I heard about it for the first time, I was confused. How can one write tests when there is nothing to test? Let’s see how it works.

TDD declares three steps:

  1. Write a test for the new functionality. It’s going to fail because you haven’t written the required business code yet.
  2. Add the minimum code to implement the feature.
  3. If the test passes, refactor the result and go back to the first step.

This lifecycle is called Red-Green-Refactor.

Some authors have proposed enhancements for the formula. You can find examples with 4 or even 5 steps, but the idea remains the same.

The problem of unit definition Suppose we’re creating a blog where authors can write posts and users can leave comments. We want to build functionality for adding new comments. The required behaviour consists of the following points:

  1. User provides post id and comment content.
  2. If the post is absent, an exception is thrown.
  3. If comment content is longer than 300 characters, an exception is thrown.
  4. If all validations pass, the comment should be saved successfully.

Here is the possible Java implementation:

``` public class CommentService {

private final PostRepository postRepository; private final CommentRepository commentRepository;

// constructor is omitted for brevity

public void addComment(long postId, String content) { if (!postRepository.existsByid(postId)) { throw new CommentAddingException("No post with id = " + postId); } if (content.length() > 300) { throw new CommentAddingException("Too long comment: " + content.length()); } commentRepository.save(new Comment(postId, content)); } } ```

It’s not hard to test the content’s length. The problem is that CommentService relies on dependencies passed through the constructor. How should we test the class in this case? I cannot give you a single answer, because there are two schools of TDD. The Detroit School (classicist) and the London School (mockist). Each one declares the unit in a different way.

The Detroit School of TDD If a classicist wanted to test the addComment method we described earlier, the service instantiation might look like this:

``` class CommentServiceTest {

@Test void testWithStubs() { CommentService service = new CommentService( new StubPostRepository(), new StubCommentRepository() ); } } ```

In this case, StubPostRepository and StubCommentRepository are implementations of the corresponding interfaces used for test cases. By the way, the Detroit School does not restrict applying to real business classes.

To summarize the idea, take a look at the schema below. The Detroit School declares the unit not as a separate class but a combination of ones. Different units can overlap.

There are many test suites that depend on the same implementations — StubPostRepository и StubCommentRepository.

So, the Detroit school followers would declare the unit in this way:

A unit is a class that can be tested in isolation from the whole system. Any external dependencies should be either replaced with stubs or real business objects.

The London School of TDD A mockist, on the other hand, would test addComment differently.

``` class CommentServiceTest {

@Test void testWithStubs() { PostRepository postRepository = mock(PostRepository.class); CommentRepository commentRepository = mock(CommentRepository.class); CommentService service = new CommentService( postRepository, commentRepository ); } } ```

The London School defines a unit as a strongly isolated piece of code. Each mock is an implementation of the class’s dependency. Mocks should be unique for every test case.

A unit is a class that can be tested in isolation from the whole system. Any external dependencies should be mocked. No stubs are allowed to be reused. Applying real business objects is prohibited.

Take a look at the schema below to clarify the point.

Summary of unit definition The Detroit School and the London school have arguments behind approaches they propose. These arguments are, however, beyond the scope of this article. For our purposes, I will apply both mocks and stubs.

So, it’s time to settle on our final unit definition. Take a look at the statement below.

A unit is a class that can be tested in isolation from the whole system. All external dependencies should be either mocked or replaced with stubs. No business objects should be involved in the process of testing.

We’re not involving external business objects in the single unit. Though the Detroit School of TDD allows for it, I consider this approach unstable. This is because business objects’ behaviour can evolve as the system grows. As they change, they might affect other parts of the code. There is, however, one exception from the rule: Value objects. These are data structures that encapsulate isolated pieces. For example, the Money value object consists of the amount and the currency. The FullName object can have the first name, the last name, and the patronymic. Those classes are plain data holders with no specific behaviour, so it’s OK to apply them directly in tests.

Now that we have a working definition of a unit, let’s move on to establishing how a unit test should be constructed. Each unit test has to follow a set of defined requirements. Take a look at the list below:

  1. Classes should not break the DI (dependency inversion) principle.
  2. Unit tests should not affect each other.
  3. Unit tests should be deterministic.
  4. Unit tests should not depend on any external state.
  5. Unit tests should run fast.
  6. All Tests Should Run in the CI Environment

Let’s clarify each point step by step.

Unit test requirements Classes should not break the DI Principle This one is the most obvious, but it’s worth mentioning because breaking this rule renders unit testing meaningless.

Take a look at the code snippet below:

``` public class CommentService {

private final PostRepository postRepository = new PostRepositoryImpl(); private final CommentRepository commentRepository = new CommentRepositoryImpl();

... } ```

Even though CommentService declares external dependencies, they are bonded to PostRepositoryImpl and CommentRepositoryImpl. This makes it impossible to pass stubs/doubles/mocks to verify the class’s behaviour in isolation. This is why you should pass all dependencies through the constructor.

Unit tests should not affect each other The philosophy of unit testing can be summed up in the following statement:

A user can run all unit tests either sequentially or in parallel. This should not affect the result of their execution. Why is that important? Suppose that you ran tests A and B and everything worked just fine. But the CI node ran test B and then test A. If the result of test B influences test A, it can lead to false negative behaviour. Such cases are tough to track and fix.

Suppose that we have the StubCommentRepository for testing purposes.

``` public class StubCommentRepository implements CommentRepository {

private final List comments = new ArrayList<>();

@Override public void save(Comment comment) { comments.add(comment); }

public List getSaved() { return comments; }

public void deleteSaved() { comments.clear(); } } ```

If we passed the same instance of StubCommentRepository, would it guarantee that unit tests are not affecting each other? The answer is no. You see, StubCommentRepository is not thread-safe. It is probable that parallel tests won’t give the same results as sequential ones.

There are two ways to solve this issue:

  1. Make sure that each stub is thread-safe.
  2. Create a new stub/mock for every test case.

Unit tests should be deterministic A unit test should depend only on input parameters but not on outer states (system time, number of CPUs, default encoding, etc.). Because there is no guarantee that every developer in the team has the same hardware setup. Suppose that you have 8 CPUs on your machine and a test makes an assumption regarding this. Your colleague with 16 CPUs will probably be irritated that the test is failing on their machine every time.

Let’s take a look at an example. Imagine that we want to test a util method that tells whether a provided date-time is morning or not. This is how our test might look:

``` class DateUtilTest {

@Test void shouldBeMorning() { OffsetDateTime now = OffsetDateTime.now(); assertTrue(DateUtil.isMorning(now)); } } ```

This test is not deterministic by design. It will succeed only if the current system time is classified as morning.

The best practice here is to avoid declaring test data by calling non-pure functions. These include:

  1. Current date time.
  2. System timezone.
  3. Hardware parameters.
  4. Random numbers.

It should be mentioned that property-based testing provides similar data generation, although it works a bit differently. We’ll discuss it at the end of the article.

Unit tests should not depend on any external state This means that every test run guarantees the same result within any environment. Trivial? Perhaps it is. Though the reality might be trickier. Let’s see what happens if your test relies on an external HTTP service always being available and returns the expected result each time.

Suppose we’re creating a service that provides a weather status. It accepts a URL where HTTP API calls are transmitted. Take a look at the code snippet below. It’s a simple test that checks that current weather status is always present.

``` class WeatherTest {

@Test void shouldGetCurrentWeatherStatus() { String apiRoot = "https://api.openweathermap.org"; Weather weather = new Weather(apiRoot);

WeatherStatus weatherStatus = weather.getCurrentStatus();

assertNotNull(weatherStatus);

} } ```

The problem is that the external API might be unstable. We cannot guarantee that the outer service will always respond. Even if we did, there is still the possibility that the CI server running the build forbids HTTP requests. For example, there could be some firewall restrictions.

It is important that a unit test is a solid piece of code that doesn’t require any external services to run successfully.

All tests should run in the CI environment Tests act preventatively. They should reject any code that does not pass the stated specifications. This means that code that does not pass its unit test should not be merged to the main branch.

Why is this important? Suppose that we merge branches with broken code. When release time comes we need to compile the main branch, build the artefacts, and proceed with the deployment pipeline, right? But remember that code is potentially broken. The production might go down. We could run tests manually before the release, but what if they fail? We would have to fix those bugs on the fly. This could result in a delayed release and customer dissatisfaction. Being sure that the main branch has been thoroughly tested means that we can deploy without fear.

The best way to achieve this is to integrate tests run in the CI environment. Semaphore does it brilliantly. The tool can also show each failed test run, so you don’t have to crawl into CI build logs to track down problems.

It should be stated here that all kinds of tests should be run in the CI environment, i.e. integration and E2E tests also.

Summary of unit test requirements As you can see, unit tests are not as straightforward as they seem to be. This is because unit testing is not about assertions and error messages. Unit tests validate behaviour, not the fact that the mocks have been invoked with particular parameters.

How much effort should you put into tests? There is no universal answer, but when you write tests you should remember these points:

  1. A test is excellent code documentation. If you’re unaware of the system’s behaviour, the test can help you to understand the class’s purpose and API.
  2. There is a high chance that you’ll come back to the test later. If it’s poorly written, you’ll have to spend too much time figuring out what it actually does.

There is an even simpler formula for the stated points. Every time you’re writing a test, keep this quote in mind:

Tests are parts of code that do not have tests.

The unit testing mindset What is the philosophy behind unit testing? I’ve mentioned the word behaviour several times throughout the article. In a nutshell, that is the answer. A unit test checks behaviour, but not direct function calls. This may sound a bit complicated, so let’s deconstruct the statement.

Refactoring stability Imagine that you have done some minor code refactoring, and a bunch of your tests suddenly start failing. This is a maddening scenario. If there are no business logic changes, we don’t want to break our tests. Let’s clarify the point with a concrete example.

Let’s assume that a user can delete all the posts they have archived. Here is the possible Java implementation.

``` public class PostDeleteService {

private final UserService userService; private final PostRepository postRepository;

public void deleteAllArchivedPosts() { User currentUser = userService.getCurrentUser(); List posts = postRepository.findByPredicate( PostPredicate.create() .archived(true).and() .createdBy(oneOf(currentUser)) ); postRepository.deleteAll(posts); } } ```

PostRepository is an interface that represents external storage. For example, it could be PostgreSQL or MySQL. PostPredicate is a custom predicate builder.

How can we test the method’s correctness? We could provide mocks for UserService and PostRepository and check the input parameters’ equity. Take a look at the example below:

``` public class PostDeleteServiceTest { // initialization

@Test void shouldDeletePostsSuccessfully() { User mockUser = mock(User.class); List mockPosts = mock(List.class); when(userService.getCurrentUser()).thenReturn(mockUser); when(postRepository.findByPredicate( eq(PostPredicate.create() .archived(true).and() .createdBy(oneOf(mockUser))) )).thenReturn(mockPosts);

postDeleteService.deleteAllArchivedPosts();

verify(postRepository, times(1)).deleteAll(mockPosts);

} } ```

The when, thenReturn, and eq methods are part of the Mockito Java library. We’ll talk more about various testing libraries at the end of the article.

Do we test behaviour here? Actually, we don’t. There is no testing, rather we are verifying the order of methods called. The problem is that the unit test does not tolerate refactoring of the code it is testing.

Imagine that we decided to replace oneOf(user) with is(user) predicate usage. An example could look like this:

``` public class PostDeleteService {

private final UserService userService; private final PostRepository postRepository;

public void deleteAllArchivedPosts() { User currentUser = userService.getCurrentUser(); List posts = postRepository.findByPredicate( PostPredicate.create() .archived(true).and() // replaced 'oneOf' with 'is' .createdBy(is(currentUser)) ); postRepository.deleteAll(posts); } } ```

This should not make any difference, right? The refactoring hasn’t changed the business logic at all. But the test is going to fail now, because of this mocking setup.

``` public class PostDeleteServiceTest { // initialization

@Test void shouldDeletePostsSuccessfully() { // setup

when(postRepository.findByPredicate(
    eq(PostPredicate.create()
        .archived(true).and()
        // 'oneOf' but not 'is'
        .createdBy(oneOf(mockUser)))
)).thenReturn(mockPosts);

// action

} } ```

Every time we do even a slight refactoring, the test fails. That makes maintenance a big burden. Imagine what might go wrong if we made major changes. For example, if we added the postRepository.deleteAllByPredicate method, it would break the whole test setup.

This is happening because the previous examples are focusing on the wrong thing. We want to test behaviour. Let’s see how we can make a new test that will do that. First, we need to declare a custom PostRepository implementation for test purposes. It’s OK to store data in RAM, what’s important is PostPredicate recognition. Therefore, the calling method relies on the fact that predicates are treated correctly.

Here’s the refactored version of the test:

``` public class PostDeleteServiceTest { // initialization

@Test void shouldDeletePostsSuccessfully() { User currentUser = aUser().name("n1"); User anotherUser = aUser().name("n2"); when(userService.getCurrentUser()).thenReturn(currentUser); testPostRepository.store( aPost().withUser(currentUser).archived(true), aPost().withUser(currentUser).archived(true), aPost().withUser(anotherUser).archived(true) );

postDeleteService.deleteAllArchivedPosts();

assertEquals(1, testPostRepository.count());

} } ```

Here is what changed:

  1. There is no PostRepository mocking. We introduced a custom implementation: TestPostRepository. It encapsulates the stored posts and guarantees the correct PostPredicate processing.
  2. Instead of declaring PostRepository returning a list of posts, we put the real objects within TestPostRepository.
  3. We don’t care about which functions have been called. We want to validate the delete operation itself. We know that the storage consists of 2 archived posts of the current user and 1 post of another user. The successful operation process should leave 1 post. That’s why we put assertEquals on posts count.

Now the test is isolated from the specific method invocations checks. We care only about the correctness of the TestPostRepository implementation itself. It doesn’t matter exactly how PostDeleteService implements the business case. It’s not about “how”, it’s about “what” a unit does. Furthermore, this refactoring won’t break the test.

You might also notice that UserService is still a regular mock. That’s fine because the probability of the getCurrentUser() method substitution is not significant. Besides, the method has no parameters. This means that we don’t have to deal with a possible input parameter mismatch. Mocks aren’t good or bad, just keep in mind that different tasks require different tools.

A few words about MVC frameworks The vast majority of applications and services are developed using an MVC framework. Spring Boot is the most popular one for Java. Even though many authors claim that your design architecture should not depend on a framework (e.g. Robert Martin), the reality is not so simple. Nowadays, many projects are “framework-oriented”. It’s hard or even impossible to replace one framework with another. This, of course, influences test design as well.

I do not fully agree with the notion that your code should be “totally isolated from frameworks”. In my opinion, depending on a framework’s features to reduce boilerplate and focus on business logic is not a big deal. But that is an extensive debate that is outside of the scope of this article.

What is important to remember is that your business code should be abstracted from the framework’s architecture. This means that any class should be unaware of the environment in which the developer has installed it. Otherwise, your tests become too coupled in unnecessary details. If you decide to switch from one framework to another at some point, it would be a Herculean task. The required time and effort to do it would be unacceptable for any company.

Let’s move on to an example. Assume we have an XML generator. Each element has a unique integer ID and we have a service that generates those IDs. But what if the number of generated XMLs is huge? If every element in every XML document had a unique integer id, it could lead to integer overflow. Let’s imagine that we are using Spring in our project. To overcome this issue we decided to declare IDService with the prototype scope. So, XMLService should receive a new instance of IDService every time a generator is triggered. Take a look at the example below:

``` @Service public class XMLGenerator {

@Autowired private IDService idService;

public XML generateXML(String rawData) { // split raw data and traverse each element for (Element element : splittedElements) { element.setId(idService.generateId()); } // processing return xml; } } ```

The problem here is that XMLGenerator is a singleton (the default Spring bean scope). Therefore, it instantiated 1s and IDService is not refreshed.

We could fix that by injecting ApplicationContext and requesting the bean directly.

``` @Service public class XMLGenerator {

@Autowired private ApplicationContext context;

public XML generateXML(String rawData) { // Creates new IDService instance IDService idService = context.getBean(IDService.class); // split raw data and traverse each element for (Element element : splittedElements) { element.setId(idService.generateId()); } // processing return xml; } } ```

But here is the thing: now the class is bound to the Spring ecosystem. XMLGenerator understands that there is a DI-container, and it’s possible to retrieve a class instance from it. In this case, unit testing becomes harder. Because you cannot test XMLGenerator outside of the Spring context.

The better approach is to declare an IDServiceFactory, as shown below:

``` @Service public class XMLGenerator {

@Autowired private IDServiceFactory factory;

public XML generateXML(String rawData) { IDService idService = factory.getInstance(); // split raw data and traverse each element for (Element element : splittedElements) { element.setId(idService.generateId()); } // processing return xml; } } ```

That’s better. IDServiceFactory encapsulates the logic of retrieving the IDService instance. IDServiceFactory is injected into the class field directly. Spring can do it. But what if there is no Spring? Could you do this with the plain unit test? Well, technically it’s possible. The Java Reflection API allows you to modify private fields’ values. I’m not going to discuss this at length, but I’ll just say: never use Reflection API in your tests! It’s an absolute anti-pattern.

There is one exception. If your business code does work with Reflection API, then it’s OK to apply reflection in tests as well.

Let’s get back to DI. There are 3 approaches to implement dependency injection:

  1. Field injection
  2. Setter injection
  3. Constructor injection

The second and the third approach do not share the problems of the first one. We can apply either of them. Both will work. Take a look at the code example below:

``` @Service public class XMLGenerator {

private final IDServiceFactory factory;

public XMLGenerator(IDServiceFactory factory) { this.factory = factory; }

public XML generateXML(String rawData) { IDService idService = factory.getInstance(); // split raw data and traverse each element for (Element element : splittedElements) { element.setId(idService.generateId()); } // processing return xml; } } ```

Now the XMLGenerator is completely isolated from the framework details.

Unit testing mindset summary 1. Test what the code does but not how it does it. 2. Code refactoring should not break tests. 3. Isolate the code from the frameworks’ details.

Best practices Now we can discuss some best practices to help you increase the quality of your unit tests.

Naming Most IDEs generate a test suite name by adding the Test suffix to the class name. PostServiceTest, WeatherTest, CommentControllerTest, etc. Sometimes this might be sufficient, but I think that there are some issues with this approach:

  1. The type of test is not self-describing (unit, integration, e2e).
  2. You cannot tell which methods are tested.

In my opinion, the better way is to enhance the simple Test suffix:

  1. Specify the particular type of test. This will help us to clarify the test borders. For example, you might have multiple test suites for the same class (PostServiceUnitTest , PostServiceIntegrationTest , PostServiceE2ETest).
  2. Add the name of the method that is being tested. For example, WeatherUnitTest_getCurrentStatus. Or CommentControllerE2ETest_createComment.

The second point is debatable. Some developers claim that every class should be as solid as possible, and distinguishing tests by the method name can lead to treating classes as dummy data structures.

These arguments do make sense, but I think that putting the method name also provides advantages:

  1. Not all classes are solid. Even if you’re the biggest fan of Domain-Driven Design, it is impossible to build every class this way.
  2. Some methods are more complicated than others. You might have 10 test methods just to verify the behaviour of a single class method. If you put all tests inside one test suite, you will make it huge and difficult to maintain.

You could also apply different naming strategies according to specific context. There is no single right way to go about this, but remember that naming is an important maintainability feature. It’s a good practice to choose one strategy to share within your team.

Assertions I’ve heard it stated that each test should have a single assertion. If you have more, then it’s better to split it into multiple suites.

I’m not generally fond of edge opinions. This one does make sense, but I would rephrase it a bit.

Each test case should assert a single business case.

It’s OK to have multiple assertions, but make sure that they clarify a solid operation. For example, look at the code example below:

``` public class PersonServiceTest { // initialization

@Test void shouldCreatePersonSuccessfully() { Person person = personService.createNew("firstName", "lastName");

assertEquals("firstName", person.getFirstName());
assertEquals("lastName", person.getLastName());

} } ```

Even though there are two assertions, they are bound to the same business context (i.e. creating a new Person).

Now check out this code snippet:

``` class WeatherTest { // initialization

@Test void shouldGetCurrentWeatherStatus() { LocalDate date = LocalDate.of(2012, 5, 25); WeatherStatus testWeatherStatus = generateStatus(); tuneWeather(date, testWeatherStatus);

WeatherStatus result = weather.getStatusForDate(date);

assertEquals(
    testWeatherStatus,
    result,
    "Unexpected weather status for date " + date
);
assertEquals(
    result,
    weather.getStatusForDate(date),
    "Weather service is not idempotent for date " + date
);

} } ```

These two assertions do not build a solid piece of code. We’re testing the result of getStatusForDate and the fact that the function call is idempotent. It’s better to split this suite into two tests because the two things being tested aren’t directly linked.

Error messages Tests can fail. That’s the whole idea of testing. If your suite is red, what should you do? Fix the code? But how should you do it? What’s the source of the problem? If an assertion fails, we get an error log that tells us what went wrong, right? Indeed, it’s true. Sadly, those messages aren’t always useful. How you write your tests can determine what kind of feedback you get in the error log.

Take a look at the code example below:

``` class WeatherTest { // initialization

@ParameterizedTest @MethodSource("weatherDates") void shouldGetCurrentWeatherStatus(LocalDate date) { WeatherStatus testWeatherStatus = generateStatus(); tuneWeather(date, testWeatherStatus);

WeatherStatus result = weather.getStatusForDate(date);

assertEquals(testWeatherStatus, result);

} } ```

Suppose that weatherDates provide 20 different date values. As a matter of fact, there are 20 tests. One test failed and here is what you got as the error message.

expected: <SHINY> but was: <CLOUDY> Expected :SHINY Actual :CLOUDY

Not so descriptive, is it? 19/20 tests have succeeded. There must be some problem with a date, but the error message didn’t give much detail. Can we rewrite the test so that we get more feedback on failure? Of course! Take a look at the code snippet below:

``` class WeatherTest { // initialization

@ParameterizedTest @MethodSource("weatherDates") void shouldGetCurrentWeatherStatus(LocalDate date) { WeatherStatus testWeatherStatus = generateStatus(); tuneWeather(date, testWeatherStatus);

WeatherStatus result = weather.getStatusForDate(date);

assertEquals(
    testWeatherStatus,
    result,
    "Unexpected weather status for date " + date
);

} } ```

Now the error message is much clearer.

Unexpected weather status for date 2022-03-12 ==> expected: <SHINY> but was: <CLOUDY> Expected :SHINY Actual :CLOUDY

It’s obvious that something is wrong with the date: 2022-03-12. This error log gives us a clue as to where we should start our investigation.

Also, pay attention to the toString implementation. When you pass an object to assertEquals, the library transforms it into a string using this method.

Test data initialization When we test anything, we probably need some data to test against it (e.g. rows in the database, objects, variables, etc.). There are 3 known ways to initialise data in a test:

  1. Direct Declaration.
  2. Object Mother Pattern.
  3. Test Data Builder Pattern .

Direct Declaration Suppose that we have the Post class. Take a look at the code snippet below:

``` public class Post {

private Long id; private String name; private User userWhoCreated; private List comments;

// constructor, getters, setters } ```

We can create new instances with constructors:

``` public class PostTest {

@Test void someTest() { Post post = new Post( 1, "Java for beginners", new User("Jack", "Brown"), List.of(new Comment(1, "Some comment")) );

// action...

} } ```

There are, however, some problems with this approach:

  1. Parameter names are not descriptive. You have to check the constructor’s declaration to tell the meaning of each provided value.
  2. Class attributes are not static. What if another field were added? You would have to fix every constructor invocation in every test.

What about setters? Let’s see how that would look:

``` public class PostTest {

@Test void someTest() { Post post = new Post(); post.setId(1); post.setName("Java for beginners"); User user = new User(); user.setFirstName("Jack"); user.setLastName("Brown"); post.setUser(user); Comment comment = new Comment(); comment.setId(1); comment.setTitle("Some comment"); post.setComments(List.of(comment));

// action...

} } ```

Now the parameter names are transparent, but other issues have appeared.

  1. The declaration is too verbose. At first glance, it’s hard to tell what’s going on.
  2. Some parameters might be obligatory. If we added another field to the Post class, it could lead to runtime exceptions due to the object’s inconsistency.

We need a different approach to solve these problems.

Object Mother Pattern In reality, it’s just a simple static factory that hides the instantiation complexity behind a nice facade. Take a look at the code example below:

``` public class PostFactory {

public static Post createSimplePost() { // simple post logic }

public static Post createPostWithUser(User user) { // simple post logic } } ```

This works for simple cases. But the Post class has many invariants (e.g. post with comment, post with comment and user, post with user, post with user and multiple comments, etc.). If we tried to declare a separate method for every possible situation, it would quickly turn into a mess. Enter Test Data Builder.

Test Data Builder Pattern The name defines its purpose. It’s a builder created specifically to test data declarations. Let’s see what it looks like in the form of a test:

``` public class PostTest {

@Test void someTest() { Post post = aPost() .id(1) .name("Java for beginners") .user(aUser().firstName("Jack").lastName("Brown")) .comments(List.of( aComment().id(1).title("Some comment") )) .build();

// action...

} } ```

aPost(), aUser(), and aComment() are static methods that create builders for the corresponding classes. They encapsulate the default values for all attributes. Calling id, name, and other methods overrides the values. You can also enhance the default builder-pattern approach and make them immutable, making every attribute change return a new builder instance. It’s also helpful to declare templates to reduce boilerplate.

``` public class PostTest {

private PostBuilder defaultPost = aPost().name("post1").comments(List.of(aComment()));

@Test void someTest() { Post postWithNoComments = defaultPost.comments(emptyList()).build(); Post postWithDifferentName = defaultPost.name("another name").build();

// action...

} } ```

If you want to really dive into this, I wrote a whole article about declaring test data in a clean way. You can read it here.

Best practices summary 1. Naming is important. Test suite names should be declarative enough to understand their purpose. 2. Do not group assertions that have nothing in common. 3. Specific error messages are the key to quick bug spotting. 4. Test data initialization is important. Do not neglect this.

But the main thing you should remember about testing is:

Tests should help to write code, but not increase the burden of maintenance.

Tools There are dozens of testing libraries and frameworks on the market. I’m going to list the most popular ones for the Java language.

JUnit The de facto standard for Java. Used in most projects. It provides a test running engine and assertion library combined in one artefact.

Mockito The most popular mocking library for Java. It provides a friendly fluent API to set testing mocks. Take a look at the example below:

``` public class SomeSuite {

@Test void someTest() { // creates a mock for CommentService CommentService mockService = mock(CommentService.class);

// when mockService.getCommentById(1) is called, new Comment instance is returned
when(mockService.getCommentById(eq(1)))
    .thenReturn(new Comment());

// when mockService.getCommentById(2) is called, NoSuchElementException is thrown
when(mockService.getCommentById(eq(2)))
    .thenThrow(new NoSuchElementException());

} } ```

Spock As the documentation says, this is an enterprise-ready specification framework. In a nutshell, it has a test runner, and assertion and mocking utils. You write Spock tests in Groovy instead of Java. Here is a simple case validating that 2 + 2 = 4:

``` def "two plus two should equal four"() { given: int left = 2 int right = 2

when:
int result = left + right

then:
result == 4

} ```

Vavr Test Vavr Test requires special attention. This one is a property testing library. It differs from regular assertion-based tools. Vavr Test provides an input value generator. For each generated value it checks the invariant result. If this is false, the amount of test data is reduced until only the failures remain. Take a look at the example below that checks that whether the isEven function is working correctly:

``` public class SomeSuite {

@Test void someTest() { Arbitrary evenNumbers = Arbitrary.integer() .filter(i -> i > 0) .filter(i -> i % 2 == 0);

CheckedFunction1<Integer, Boolean> alwaysEven =
    i -> isEven(i);

CheckResult result = Property
    .def("All numbers must be treated as even ones")
    .forAll(evenNumbers)
    .suchThat(alwaysEven)
    .check();

result.assertIsSatisfied();

} } ```

Conclusion Testing is a significant part of software development, and unit tests are fundamental. They represent the basis for all kinds of automation tests, so it’s crucial to write unit tests to the highest standard of quality.

The biggest advantage of tests is that they can run without manual interactions. Be sure that you run tests in the CI environment on each change in a pull request. If you don’t do this, the quality of your project will suffer. Semaphore CI is a brilliant CI/CD tool for automating and running tests and deployments, so give it a try.

That’s that! If you have any questions or suggestions, you can text me or leave your comments here. Thanks for reading!

The post A Deep Dive into Unit Testing appeared first on Semaphore.

View Details

When your engineering team grows from 10 to 100 engineers in the course of a year, there are so many things that you need to focus on, from operations and developer tooling to testing. Maintaining the health of the application is perhaps the most difficult part of all. Where exactly do you start?

We sat down with Justin Searls, the co-founder and CTO of the Test Double agency. For many years, Justin has been consulting organizations on how to best tackle team’s growth and ensure that good practices are in place when teams grow.

We talked about how to grow engineering teams without losing sanity, how to divide work without stepping on one’s toes, and keep your test suite maintainable.

Here’s a brief overview of what we covered in our conversation:

  • How does software let people down?
  • How to scale up your test suite when your team grows tenfold
  • Rearchitecting things when team scales up
  • Conway’s law
  • Rapid growth of an engineering team: patterns and anti patterns

Listen now or read the edited transcript.

You can also get Semaphore Uncut on Apple Podcasts, Spotify, Google Podcasts, Stitcher, and more.

Like this episode? Be sure to leave a ⭐️⭐️⭐️⭐️⭐️ review on the podcast player of your choice and share it with your friends.

Introduction Darko Fabijan (00:02):
Hello and welcome to Semaphore Uncut, a podcast for developers about building great products. Today, I’m excited to welcome Justin Searls. Justin, thank you so much for joining us.

Justin Searls (00:12):
Hey, thanks for having me. For anyone not familiar with me and what I do, I am the co-founder and current CTO of a software agency called Test Double.

I’m a lifelong consultant. I came out of college and worked at a big consultancy, $2 million projects and financial institutions, and slowly worked my way down to smaller teams.

Through my various experiences as a consultant, I have learned all kinds of ways that project teams fail to hit deadlines. They struggle to make high quality systems and code. They also struggle to develop and communicate shared values and normalize on conventions.

At the same time, they are underappreciated by an industry that tries to oversimplify what software development is. In a way, there’s this metaphor for some sort of industrial or manufacturing process that can be easily distilled down and automated. So I’ve been on a mission to surface all the ways that software lets people down.

How does software let people down? Justin Searls (01:31):
If you follow me on Twitter, you just see me posting bugs left and right. It’s to say that we’re in a pretty broken industry and we need to both build awareness and start to think together about what we can do to make it better.

We’re in a pretty broken industry and we need to start thinking together what we can do to make it better.

-Justin Searls, co-founder and CTO of Test Double

I think the only way forward is to have a pragmatic and optimistic lean towards figuring out creative solutions to keep the industry moving.

That led me to focus on lots of secondary tasks that developers are tasked with, like operations, developer tooling and testing, agile process and helping product owners best articulate the needs that they have. How do we solve secondary and tertiary concerns that are outisde a particular algorithm?

That’s where I’ve spent most of my time.

How to scale up your test suite when your team grows tenfold Darko Fabijan (02:52):
Thank you for that great introduction!

I’m going to use the opportunity to introduce us through the topic. We’ve been doing CI/CD for the last 10 years. In the last couple of years, we’ve been very lucky to have some very successful clients. For instance, they’re a team of 20-30 engineers doing something and having a common understanding how to do things.

But then, success hits the team, and they have to scale to a hundred or two hundred developers over a year or two. We love having those customers. But we also want to help them as they’re facing various challendes regarding the health of their test suite, scaling of their test suite, etc.

Flaky and brittle tests are also in that category and level the expertise and technical maturity of the team as a while. We tackle the hardest topic of all: how to make sure that as applications are growing, good practices stay in place. How does the team that’s growing rapidly level up the health of the test suite?

The question is where to start.

Justin Searls (04:31):
You touched on something that’s really important. In a lot of companies, especially SaaS, the marginal cost of onboarding a new customer is typically near zero. That means that if you get a sudden influx of thousands of paying customers, you’ll have to think about how to scale up things like support, etc.

Since the marginal cost of acquiring new customers is almost nothing, it led our industry to make a fallacious assumption that we can just scale up all these other things, too. That might be true of AWS resources. We can scale up the server count, the CPU count and the amount of RAM on a little slider.

However, scaling up a team from 25 people to a hundred people in a rapid way requires a lot of care and communication. How do we divide up the work in a way that we’re not stepping on each other constantly?

As a business owner or a manager, I have to figure out how to solve a thousand things at once.

Rearchitecting things when team scales up Darko Fabijan (07:25):
When teams rapidly grow from 25 to a hundred people, architecture of the application might not be made in such a way as to support this growth. People struggle to make sure that they have a reliable CI/CD in place. And we’re here to help.

What ends up being part of the problem is the architecture of the test suite that usually gets inverted. There are hundreds and thousands of very expensive tests that are not needed. People struggle to scale CI/CD along with their teams and their architecture.

Do you have any advice on how to scale applications made by a team five time smaller than the team which now has to run with it?

Conway’s law Justin Searls (08:43):
Yeah, there’s two big things. First, I often find myself referring to Conway’s law, which is that our technical systems tend to reflect the human organizations that create them. If your application has lived for seven years and just 5 or 10 people were working on it, the systems from the architecture of the code is going to be optimized for both a team that size of 5 to 10 people as well as the individual proclivities and the dynamic and the chemistry of that 5 to 10 people.

According to the Conway’s law, our technical systems tend to reflect the human organizations that create them.

-Justin Searls, co-founder and CTO at Test Double

There’s a transition that happens as we encode our preferences and our patterns of behaviour into the things that we automate.

If a team grows from 5 or 10 to a hundred people in the course of a year, it’s a growth sprut that has no analog in biology. Conway’s law is a constraint where the speed at which the system can catch up to the organizational change is going to be limited.

That means that we have to be mindful of any mismatch between our human structure and the technical architecture that we have in place. I might have a monolithic application where every test by default just uses a browser and goes through the full stack of everything just to make sure that the feature works. Or maybe I’ve got a separate browser test for every single resource in my crud application.

As I scale linearly, the system gets slower and the tests get more numerous. Even tests that used to run in a second now take two seconds to run.

So the system gets bigger, everything takes longer. Going from 10 tests to a hundred, you might notice the build slowing down by a factor of 20, 30 or 40, not 10. That’s where we need to be mindful of the inflection point that we’re in. If we’re undergoing rapid growth, we need to pause and think about what does scaling with stability look like, what do we have to start and stop doing as developers.

Rapid growth of an engineering team: patterns and anti patterns Darko Fabijan (12:05):
When it comes to rapidly growing a monolithic applcation, what are some of the patterns and antipatterns that you’d recommend or notice?

Justin Searls (12:23):
One of the things that is a super power of a batteries included framework like Ruby on Rails for example, is that you can if you just follow of the cookie cutter objects that it gives you, in a single controller action, you can accomplish 15 different things at once. You can handle, of course, the literal behavior of updating something in the database, you can handle cross-cutting concerns like logging and authorization of role. You can do air handling, and you can do additional checks.

Justin Searls (12:56):
You can just keep on adding lines to each of those controller actions as they get more complicated. And that is highly productive when you’re starting out.

But you have to be mindful of any time that you combine or you embed multiple responsibilities or reasons that code could change down the road into a very tight little function or method or unit, it’s going to create a tangle that’s going to result in future people.

If you’ve got multiple people working on it, having reasons to go into that code at the same time, which is going to lead to tests that contradict one another in separate feature branches, or you’re going to have version control conflicts, or you’re going to have unspecified behavior where maybe each person’s tested their individual corner of a piece of functionality, but never really thought about it from the perspective of a stakeholder or how the system’s actually used.

Justin Searls (13:53):
And once you get into production, you realize, oh, there’s something in the cookie in production because of our new A/B testing software and no one really thought about that.

And so of course the value that wasn’t present in either of our testing environments is now somehow interacting with these other features. And so the first advice that I give is to start aggressively de-tangling and choking each of the responsibilities that any feature performs into isolated, small, well-named units that are then composed with a level of abstraction that is consistent so that you can have a 30,000 foot view of what a particular feature does.

The first advice that I give is to start aggressively de-tangling and choking each of the responsibilities that any feature performs into isolated, small, well-named units that are then composed with a level of abstraction that is consistent so that you can have a 30,000 foot view of what a particular feature does.

-Justin Searls, co-founder and CTO of Test Double

Justin Searls (14:34):
And it just says on the tin I do X and then I do Y and then I do Z. Then you can dive deeper into a tree, whether those are literal files and folders of subordinate behavior that supports that feature, as opposed to just continuing to grow longer and longer functions with more and more ifs and else’s or overly clever abstractions that maybe take advantage of a lot of clever stuff like reflection and introspection to dynamically handle cases that make it harder to grip around.

So I guess the first thing that I talk about is writing simpler, more obvious single purpose code.

Darko Fabijan (15:55):
The majority of people regardless of the framework can end up in this situation, so the advice is pretty universal. Previously, we’d need to set up a lot of things, a whole environment, and then run through that controller action. That can cause a lot of headache along the way.

Justin Searls (16:36):
Last week, I wrote two new gems. One that integrates with the Pipedrive CRM, and one that defers tasks to the end of an HTP request so that they don’t block the user form receiving their HTP response called later.

I’m using those two gems that I made in conjunction to when somebody fills out a contract form at testdouble.com to greet us, they can immediately get a confirmation that an email will be sent to us.

But when I came to test that, I started thinking, well, maybe the later gem by default is asynchronous and goes to another thread?

Speaking of me making gems, I make a lot of gems. I made a new mocking library called Mocktail for Ruby last year, which I’m really excited about. Maybe this is an opportunity to use that and realize that mocktail is thread safe. And so I got to inline all of this stuff.

Justin Searls (18:21):
So it’s like, oh, maybe this is an opportunity to use that and then realize like, oh yeah, mocktail is thread safe. And so I got to inline all of this stuff.

And so I add inline features to the maybe later gem and then I realized, oh right, puma’s on yet another thread. And so like all of that, just to answer the question of did this code get run the way that I expected from the parameters that I passed in?

And I could have saved myself all of that pain if I had chosen to test just one layer beneath the controllers. If I had just decided, hey, instead of slamming all of this behavior into a controller action, what if I just had a single class called handles contact form submission or something?

And if the code is all there, then I am in control of just invoking it directly and calling it like a normal object.

Justin Searls (19:04):
I have all of the power in the world then to very, very carefully ascertain both the response that I get back and any sort of secondhand signals that it sends off.

But that betrays I think the impulse of a lot of testers, a lot of people who are excited about testing is like they want to have a maximally realistic test. They want to know that when I load that webpage, this thing is actually happening, and that is valuable and it’s worth knowing, but I think that you need to calculate the overall return on an investment of the activity.

If you’re able to get to a point where each of your controller actions are all just one liners calling to normal plain old objects that are easy to put under test with just an indication, then I don’t know, you can just hand wave each of these frameworky classes that just handle the routing of stuff and call that, that’s just configuration.

Justin Searls (19:58):
And if that layer stops working, the website won’t load. So we can probably just not have every single test redundantly exercise all of this HTTP complexity that makes everything so much more difficult for us to ascertain.

Those are the kinds of trade offs that I think a mature team can navigate. But it is really difficult when you’re growing really quickly and you don’t have that experience and you don’t know what sort of negative consequences await you if you just continue to add more naive, highly integrated tests.

How to keep tests from becoming unmaintainable Darko Fabijan (20:38):
Just a week or two ago, I was speaking with creator of Cucumber, a BDD testing framework, Aslak Hellesoy, and he was coming back to exactly what you in your example now demonstrated.

We want to get a maximum reliability and we also want speed. We also want to cover a business logic in the UI and combine different things together, we will get entangled into something.

There are too many constraints or too many things that we want to solve and that’s how we could end up with test suite, which is just unmaintainable.

Justin Searls (21:17):
Yeah. And that’s I think one of the reasons why an interdisciplinary profession like ours where we have a technical component and we have an interpersonal component to the work that we do as developers every single day. If it was all interpersonal, then we wouldn’t have a rug under which to sweep a lot of uncomfortable decisions.

But because we have that technical component and because our test suite can’t call us out in a retrospective meeting or can’t advocate for itself that like, hey, I shouldn’t be wasting my time on this redundant action, I can choose to look at all the trade offs like you just identified and say like, well, let’s just have our cake and eat it too and just continue to shove all of this stuff into our test suite.

And that can kick the can down the road on either maybe a hard conversation between me and a colleague who we value different things about the tradeoffs that we’re making.

Justin Searls (22:22):
I remember 10 years ago back in the olden days before cloud CI was a normal thing, I had a client whose build logically took like three hours to run. It was a really, really long build. And so that wasn’t fast enough feedback.

They also had just hired like hundreds of developers and so they had to decide, do we change fundamentally how we do our work?

When I was a kid, we used to rub wax paper on slides in playgrounds to make them go faster. Do we just rub wax paper on this build and make it go faster?

-Justin Searls, co-founder and CTO of Test Double

Or do we like, when I was a kid, we used to rub wax paper on slides in playgrounds to make them go faster, do we just rub wax paper on this build and make it go faster?

The AWS was new and that’s where they were running stuff. And so what they did was they just added as many AWS instances, EC2 instances, as they had integration tests. So basically every single Cucumber test had its own instance and they just kept those spun up all the time.

Justin Searls (23:13):
And so they were able to get that logical three hour build down to whatever the slowest individual test was. And they got it down to like 20 minutes or something like that.

The problem then was that everyone kept working in exactly that same way, learning no lessons and that super linear test build problem came back.

And so you revisited a year later and now if you were to like actually find an ordering bug in their tests and try to run them all in order to debug that or to bisect what is this test pollution that’s causing this brittleness. I think that it had gone from three hours to like 28 logical hours end to end.

Justin Searls (23:46):
The team had sufficiently snuffed out any real feedback about what was going on under the hood. People would’ve seen a new rack of servers coming into an ops room and ask questions about why we are suddenly spending hundreds of thousands of dollars a month on CI servers.

When hard questions can be pushed into invisible spaces, we need to be very mindful that we are not ourselves doing a disservice to just dealing with the hard question right now.

Darko Fabijan (24:32):
Clear. Absolutely. If something can be seen with eyes or not is one of the curses of our craft. And now with hidden racks of servers, that’s an additional element to it.

Trainings that Test Double offers I want to go back. You mentioned that one of the things that you are doing some trainings with the teams and you also work with many companies that people know about, GitHub being one of them, can you share some of your experiences and what are the situations where people invite you to come in and help them to level up their team in various ways?

Justin Searls (25:10):
Yeah. Thanks for asking that. Test Double, our consulting agency, we’ve been around for about 10 years now, and we’ve been super fortunate in the last few years to be trusted and brought in by engineering organizations that we truly love and respect. And they’re doing amazing things.

Companies like GitHub, Zendesk, Betterment, these are organizations are doing great work. It’s not so much that they see that our engineers when they join their team have some capability that they lack or that they’re insufficient in some way. They just understand that there are so many facets to building great software that getting some outside perspective sometimes is necessary and valuable.

And so to your point, a lot of times a client will tap me specifically and ask me to kind of like just do an assessment of how are we testing or try to think deeper about repeated systematic failures that the team is running into that they don’t have any creative solutions for.

Justin Searls (26:14):
And for anyone listening, what I can say is no one has this stuff figured out and I am not really sure that there is a one size fits all solution. Because the more that you grow as a company and the more that you choose to automate stuff to make the business run, the more that those automations are going to be a tight fit for the particularities of your business. If you’re just writing commodity code, it’s not differentiating. You may as well just use some off the shelf piece of software.

The more that you grow as a company and the more that you choose to automate stuff to make the business run, the more that those automations are going to be a tight fit for the particularities of your business.

-Justin Searls, co-founder and CTO of Test Double

Justin Searls (26:50):
Us as developers, every time that a product owner is asking us to do something, 99% of the time they’re asking us to invent something that’s never been done before, at least not in the context of this particular business.

And so we need to have a humility. For example, I’m in a training with a bunch of developers who are smarter than me, and they’re looking at like, hey, we’ve got all of these flaky tests and we don’t know why, but something like 2% of tests will just always fail on every given build, meaning we can never get just a clean run and a green build.

And we run them out of order and we run them in a parallelized way to try to identify test pollution, but ultimately end of the day, what are the strategies that we can use to tackle that?

Justin Searls (27:34):
And because every company has a completely different blend of services under the hood and priorities for are what they need to check in order to feel comfortable about deploying to production, it’s important that we recognize that you might as a developer feel pride in a particular algorithm that optimizes the core thing your business does. Maybe it’s coming up with creative ways to schedule something in logistics and that’s your special sauce.

It has to become our special sauce to think about how to tackle these meta problems like an exploding CI build duration, or the number of flaky tests. It might mean that we need to actually develop bespoke tools to identify, ah, yeah, we need to categorize these tests are flaky and once you’ve marked it, a human marks it, and then it goes into an optional suite, which goes into a queue, which then somebody can take a closer look at and try to analyze for data pollution.

Justin Searls (28:33):
That way you’ve taken the immediate pressure off so that somebody who’s got the clarity of mind to focus and really problem solve about each of the individual problematic tests can do that without it simply being like, oh crap, I got to release, I got to release, hair on fire. I got to push, push, push, push, push. What can I do to shut this test up so that I can get my feature out.

Justin Searls (28:54):
Responding to the particularities of your company and the urgencies that it’s under and the priorities that it has is the only way to understand what kind of solution is going to work for us particularly. That might require you to develop new processes or invent new tools or new workflows that you’re not going to find in a blog.

Or you’re not going to find an off the shelf solution for, and it’s going to take time and hard thinking and permission to spend and invest real time on it, even though it’s maybe not a feature that a customer’s going to see, the customers and the business will benefit from a smoother, lower friction ability to deploy and deliver new software.

Justin Searls (29:35):
And so it’s like we just have to acknowledge that’s part of our job as developers, and also get better at making that case to explain the value of these kinds of activities to the people that we’re working with who maybe aren’t so technical or don’t feel that pain as acutely

Darko Fabijan (29:50):
Great explanation. And also I think a great insight into what technology leaders, people who are leading the team recognize that these challenges are not something that they might be facing particularly in their team. It’s an industry-wide problem.

Just an example. Fairly often people ask, “Okay, are your other clients facing the same thing? Facing the same challenges?” And yeah, of course they are. A lot of them are dependent which level of transparency we are going to discussion and so on, but yeah, all great advices and tips.

Justin, if our listeners want to learn more on these topics, related topics, what are some pointers where they can find more about you, your talks, generally your consulting work.

More places to learn about Justin’s work Justin Searls (30:39):
Yeah, thank you. So I tweet a lot just kind of top of mind stuff as I run into it as my last name Searls on Twitter. If you’re looking for kind of more substantive stuff, most of the things that myself and my colleagues spend a lot of time thinking about, we’ve written about either at our blog, which is blog.testdouble.com or we’ve got open source tools for us.

So you can find us on GitHub and check out all of the dozens of repos of often small libraries that just solve particular problems that we run into at clients.

Justin Searls (31:12):
And then increasingly over this year in particular, we’re planning on building more practical content to help developers on YouTube. So we’ve got a YouTube channel, it’s just also under the Test Double tag. And I hope you follow us and stay tuned for the stuff that we’re doing there because honestly, sometimes I view the consulting business as a way to fund our helping developers for free and public habit, and I’m just thrilled that we get to spend the time to share some of the things that we’re learning while we do the work.

Darko Fabijan (31:41):
Yeah. Great. Thanks for that. Thanks for all the tips and advices and yeah, good luck.

Justin Searls (31:47):
Yeah. Thanks so much. Thanks for having me.

The post Justin Searls: How to Grow Your Engineering Team Tenfold in a Year and Keep Test Suites Healthy appeared first on Semaphore.

View Details

At my previous job at a big multinational company, we didn’t have anything remotely resembling an automated test. What manual testing we did consisted of dragging and dropping a folder and mashing F5. When we skipped testing, however, the users ended up doing it for us. After they did their round of testing, they would call or show up at our office, wondering angrily why the product was not working as they expected.

I start with this story because I don’t want you to think that I do not value tests. I do. Very much. I believe we can’t live without them. But you can have too much of a good thing. However vital water may be, too much of it results in a flood.

You get a test, you get a test, everyboooody gets a test! Everybody gets a test… Tests are living documentation, messages to other developers (and our future selves) about what the code is supposed to do. Tests enforce requirements and provide feedback, telling us when something breaks.

A testament to their power is that at least two test-centric philosophies have materialized: Test-Driven Development (TDD) and Behavior-Driven Development (BDD). Both prescribe that we should always write the test before the code. Only then are we allowed to work on implementation.

The TDD Cycle The startling thing about both paradigms is that they’re not about eliminating bugs (although they help reduce them). They’re about design. Writing the test first forces us to think from the user’s point of view (even if that user is only us at the beginning). The fact that we end up with an executable test is mostly just a nice side effect.

Testing’s dark side Photo by R.D. Smith There are two ways of thinking about this: testing in general and testing first.

When faced with a practice widely advertised as “one size fits all” or “the one true way” like TDD or BDD, I think a bit of skepticism is only healthy. Every framework has its limitations. So what tradeoffs are we making by putting so much emphasis on testing?

  • Investment: an ever-growing test suite has maintenance costs. If the time invested in writing a test nets a positive return, then you’ve added value. But there are no guarantees that the ROI won’t be negative at times.
  • Less readability: testable code is often less readable. Abstractions like inversion of control, dependency injection, mocking, or stubbing are common and make for hard-to-read code.
  • No silver bullet: at times, code makes us jump through hoops to write a test. For instance, a controller in an MVC application needs extensive mocked requests and responses, even to test a little bit of logic.
  • Prototyping: until you have a working prototype, testing is more of a burden than a boon. Testing starts to make sense once the system matures and interfaces stabilize.
  • False positives: tests are also code and they can be buggy, causing false positives and making you hunt for nonexistent bugs.
  • Green mania: I call this the compulsion to reach 100% coverage. While seeing a full green bar is satisfying, no amount of tests can prove that a system is error-free. Testing everything can generate the illusion of a perfect codebase, but it doesn’t mean that there are no problems.
  • Speed: while there are methods to make tests run very fast, they do still take time and resources to run. So you should make each one count.
  • Cargo cult: some developers feel shame if they don’t write tests for everything.

To those who prescribe “test everything, always,” I ask: is the design better because of the test or because I stopped to think before coding?

The goal is to write code that works We should be open to admitting that tests may not be necessary for every single line of code. While it takes experience to know how to write tests, it takes even more to know when and where they should be written.

So, instead of development being test-driven, wouldn’t it be better if it was thought-driven? We should always be thinking about what we’re doing before putting our hands on the keyboard. Here are some things that you should keep in mind while writing code and trying to decide what you want to test:

  • Do I have a fast test suite? Does your CI/CD pipeline run in under 10 minutes? If not, allocate some effort to streamlining it before extending the suite.
  • Am I following a specification? Is the test covering business requirements? Do you have a specification defined? If so, you must write enough tests to ensure you’re following it.
  • How critical is my code? Not every piece of code is equally important; some may not even deserve a test. Consider what’s the worst that can happen if your code has errors.
  • Is my code interesting? Is the code under test complex enough to warrant a test? Avoid testing trivial code, and make sure you’re not testing the compiler.
  • Am I heavily refactoring only for the sake of testing? How difficult is it to implement a test? In other words, is the test more complex than the actual code tested? If you need to add several levels of ad hoc code, maybe the cure is worse than the disease.
  • Do I control all elements in the test environment? Do you fully own the system? External dependencies lead to flakiness. If you depend on third parties, maybe you need some form of contract testing instead.
  • Do I need excessive setup? Do you need to set up a ton of things to test? It could mean that your code is too tightly coupled, or you’re trying to do unit testing where integration or end-to-end tests would be a better fit.

As you can see, exactly when to write a test is a question filled with nuances. When there are no clear answers, perhaps going back to basics helps:

I get paid for code that works, not for tests, so my philosophy is to test as little as possible to reach a given level of confidence (I suspect this level of confidence is high compared to industry standards, but that could just be hubris). If I don’t typically make a kind of mistake (like setting the wrong variables in a constructor), I don’t test for it.”

Kent Beck at StackOverflow Creator of Extreme Programming and leading proponent of TDD.

Final thoughts Kent’s quote should resonate every time a developer sits down to write code. The key factor here is self-awareness of your own level of proficiency. A rookie developer will need to write more tests than a veteran because they’re more likely to make more basic errors. With experience, a stronger sense of where you need to test emerges.

Testing is a tool. Applying this tool means knowing when the benefits outweigh the costs. Some people argue that everything should be tested, including static views. Others draw the line at presentational elements. Use a thought-driven approach to better decide where your line is. Use a thought-driven approach to better decide when tests should be written.

Thanks for reading.

The post Do You Need to Test Everything? appeared first on Semaphore.

View Details

Smoke testing is a technique for discovering major software flaws using CI/CD pipelines. It consists of very shallow but broad tests that verify the crucial parts of the application. This means that smoke testing is typically a very cost-effective operation.

In this article, we will explore the origins of smoke testing and how it can be incorporated into your continuous integration or continuous delivery pipelines.

Origins of smoke in testing The term “smoke test” originally comes from electrical engineering. Once the circuit of an electrical device is ready for testing, it is attached to a power source. If there are major issues, the circuit will start smoking immediately. This shows that the system must first be fixed before any other tests are undertaken.

The same concept has been adopted in software development, together with the original term.

Photo by Nicolas Thomas on Unsplash What and how we should test Smoke tests should determine if the system is stable enough for it to proceed to the next phase, i.e. to more granular tests. This is achieved by testing the system’s main functionalities at a fundamental level.

Smoke tests don’t need to be comprehensive. For example, if we are working on an e-commerce web application, we only need to check that customers can buy a product. We don’t need to verify all the details and edge cases of the purchasing process.

Although it is common to smoke test the main user-facing features of an application, smoke tests are also applicable at many different abstraction levels throughout your entire system. The value of this multi-level smoke testing approach is that you can discover defects on a more controlled and granular level. When working on distributed systems that involve many teams, this can end up being a big time saver.

Depending on what you are testing, the actions that you will take based on the results you get can vary greatly. However, smoke tests must be fast in order to ensure a fast feedback loop. The value of a smoke test lies in being able to fail very fast, so you don’t waste time and resources on unit and acceptance testing when there is a fundamental problem with your project.

Tools that serve well for writing smoke tests can be very simple. For instance, a simple curl api.myapp.com/is_alive is a good start. Checking versions of installed dependencies with a simple Bash script could also be sufficient.

For more complex checks, you can rely on a testing library in your favorite programming language. If you are using Ruby, for example, and want to test some of the main features of your web application, you can run a few Cucumber scenarios against your production instance.

Where Should We Smoke Test? Within a CI/CD process, there are at least four places where smoke tests fit in:

  • Before committing code — allowing developers to run smoke tests locally.
  • Before end-to-end and acceptance tests (and other long-running tests) — checking that the application can boot up.
  • Before deployment — validating the configuration and infrastructure.
  • After deployment — ensuring that the application is running and testing basic functionality.

The following diagram shows where these points are located:

Local smoke testing The first possible place for smoke tests is, in fact, before any CI/CD. Developers can run tests on their machines before pushing code to the repository. A Git test script coupled with a client-side hook should be enough to automate the process.

Git hooks are little scripts that run upon seeing certain events. To run a smoke test when a developer attempts a commit, first create a pre-commit executable file inside the .git/hooks folder in your repository:

$ touch .git/hooks/pre-commit $ chmod +x .git/hooks/pre-commit

The hook can then call the smoke test script and evaluate its exit code:

```

!/bin/bash

.git/hook/pre-commit

executes smoke test previous to commit

./scripts/smoke-test-local if [ $? -ne 0 ]; then echo "Commit is rejected due to smoke test failure" exit 1 fi ```

Building and booting up an application on a developer’s workstation is made more approachable by the fact that we don’t need a complete runtime environment to conduct our smoke test. We can often get an environment that is “good enough” with an in-memory database and mocked components.

You may think that this smoke test is a waste of time. After all, there are probably hundreds of other tests in your pipeline that go much deeper than just checking that the application can start. But don’t be fooled into thinking that smoke tests are useless due to their simplicity. Your code might pass unit and integration tests with flying colors and still fail to boot up. Smoke tests are your code’s first instance of a reality check.

Smoke tests in the CI pipeline The next logical point for a smoke test is in the build stage. In this stage, we build the binary/executable artifact and run various types of tests. Often, we can reuse the same script used to run the Git hook smoke tests.

The point of this check is to confirm that the application can boot up and perform some basic functionality, but this time in the clean environment that the CI machine provides. Ideally, you build the artifact once (and only once) and test it over the course of the pipeline. The test does not need to be exhaustive, but it must be able to expose major problems.

Smoke tests as a part of the acceptance test suite Once basic testing is done, we can tackle more comprehensive items such as acceptance tests and end-to-end tests. These types of tests usually take a long time to set up and run, because they need a production-like environment and they interact with the application via the UI.

Because of this longer runtime, failed acceptance tests are more frustrating than most other types of tests. Smoke testing presents another opportunity to fail fast and potentially save time. A few well-placed smoke tests that verify the most critical application paths can catch most problems before we begin the end-to-end testing phase.

Pre- and post-deployment tests In some cases, we can run acceptance smoke tests directly in the CI pipeline, while in others — like in our example — we can use a separate staging environment.

A staging environment presents an opportunity to test the deployment procedure. This process benefits from smoke tests at several points:

  1. We can perform a configuration smoke test and infrastructure smoke tests to validate that the required services are online and reachable in the network.
  2. We can deploy the application in the staging environment.
  3. A post-deployment smoke test validates that the application has correctly started and is healthy.
  4. Smoke tests have passed, so the application does not seem to have any major problems. The final step is to run the acceptance or end-to-end test suite for a more in-depth verification.

Smoke tests in DevOps Production deployment introduces a few more opportunities for smoke tests.

Like we did during acceptance testing, we’ll sanity check the environment with config and infrastructure tests, then deploy and run a post-deployment smoke test.

  1. Configuration smoke test — validates the configuration before deployment. Some basic sanity checks can prevent the application from starting with the wrong settings.
  2. Infrastructure smoke test — checks various elements that can disrupt the deployment or prevent the application from starting up once deployed. With a good smoke test, you can quickly and cheaply save yourself from introducing broken servers into production. Infrastructure smoke tests include: – Checking that servers are online. – Pinging network ports and checking that firewalls are open. – Checking DNS records and SSL certificates. – Trying out API endpoints. Validating some of their responses. – Performing queries on databases.
  3. Post-deploy smoke test — checks that the application is running. Failures at this point can trigger a rollback process, possibly using some form of canary or blue-green strategy.

A few examples of smoke testing In this section, we’ll move on to a more concrete example and try to apply everything we’ve learned so far.

Let’s say we’re writing an API server and that we already have a working CI pipeline. We now want to extend the process with continuous delivery.

First, we’ll need a small test script. Pinging one of the endpoints with curl should be enough for a basic smoke test:

```

!/bin/bash

check-app-up.sh

Checks that the URL $APP_URL/is_online returns HTTP status 200

set -e status=$(curl --silent --output /dev/stderr --write-out "%{http_code}" "$APP_URL"/is_alive) if [ $status -ne 200 ]; then echo "Got status code $status instead of expected 200" exit 1 fi ```

Next, we have to prepare the CI machine to run the application. The steps to do this are as follows:

  1. Retrieve the built binary from the artifact store.
  2. Start any dependent services, such as a database.
  3. Boot up the application.
  4. Run the smoke script and capture the result.

We can fulfill the steps above with the following commands:

```

clone the repository

checkout

get the compiled binary from the artifact store

artifact pull workflow myapiserver.bin

prepare a test db with sample data

sem-service start postgres psql -U postgres -c "CREATE DATABASE myApiDB;" psql -U postgres -d myApiDb -c "COPY mytable FROM 'sample_data.csv' WITH (FORMAT csv);"

start and smoke test the application

export APP_URL=localhost ./myapiserver.bin & bash scripts/check-app-up.sh

```

Once the smoke test job is introduced, the pipeline looks like this.

Smoke testing deployments Encouraged by the success of the first smoke test, the next step is to deploy to the staging environment and run more tests. In our example, there are two items to check before we can say that the infrastructure is ready:

  • The server should be up and reachable.
  • The database should be ready to accept connections.

Both checks can be solved with small Bash scripts. First, we check the server via SSH:

```

!/bin/bash

check-server-up.sh

Checks that remote server is reachable at $SERVER_ADDRESS

and that it's hostname is equal to $SERVER_HOSTNAME

set -e remote_server_name=$(ssh "${SERVER_ADDRESS}" hostname) if [ ! "$remote_server_name" = "$SERVER_HOSTNAME" ]; then echo "Server name doesn't match: expected $SERVER_HOSTNAME found $remote_server_name" exit 1 fi ```

Second, to ensure the PostgreSQL database is ready, we can use pg_isready:

```

!/bin/bash

check-db-up.sh

Checks that PostgreSQL database is ready

PGPASSWORD="${DB_PASS}" pg_isready \ -h "${DB_HOST}" \ -p "${DB_PORT}" \ -d "${DB_NAME}" \ -U "${DB_USER}" ```

The following screencast shows how to add a continuous deployment pipeline with two smoke tests and a deployment job.

Below you’ll find the YAML snippet for the pipeline for reference. You’ll need to create a couple of secrets to store sensitive data, such as SSH keys and database credentials, to complete the setup.

version: v1.0 name: Continuous Deployment agent: machine: type: e1-standard-2 os_image: ubuntu1804 blocks: - name: Pre-deploy test task: jobs: - name: Server & database commands: - checkout - bash ./scripts/check-db-up.sh - bash ./scripts/check-server-up.sh env_vars: - name: SERVER_HOSTNAME value: myserver-prod - name: SERVER_ADDRESS value: api.myapp.com secrets: - name: db-params - name: ssh-keys - name: Deploy Application task: jobs: - name: Deploy commands: - checkout - bash ./scripts/deploy.sh env_vars: - name: SERVER_ADDRESS value: api.myapp.com secrets: - name: db-params - name: ssh-keys - name: Post-deploy test task: jobs: - name: Smoke test commands: - checkout - bash ./scripts/check-app-up.sh env_vars: - name: APP_URL value: api.myapp.com - name: APP_PORT value: '80'

Try the continuous deployment pipeline to see the smoke tests in action.

At this point, it should be fairly obvious how to extend the workflow with a production-deployment pipeline. You should repeat the steps and direct the deployment script to your production environment.

Would you like to see smoke tests and deployments in action? Let me know on Twitter and I’ll prepare and link a ready-to-use demo project.

Conclusion Smoke tests should evolve as your application grows. In the beginning, they check one or two essential functions and, over time, grow to serve the purpose of catching errors quickly and cheaply. It takes a bit of effort but can save a lot of time and headaches.

The post Smoke Testing Keeps Your Delivery Pipeline Safe and Sound appeared first on Semaphore.

View Details

The word ‘test’ originally referred to “a small vessel used in assaying precious metals”. This meant that testing was a method of ascertaining the quality of gold or silver. It was also used in the process of refining valuable alloys, such as tin.

Later, the term was adopted in other fields, and these days it’s common to find it in contexts such as education, medicine, or software development. Its essence, however, has not changed: testing is used to refine end value.

We use tests in software development to ensure that code works as expected. Tests can be manual or automated. Manual testing is similar to automobile manufacturers crashing cars to verify that they’re safe for the road. It works, but it’s far too expensive to do frequently, so it is typically done at the end of the production cycle. The trouble with this method is that problems found at this stage can delay a product’s launch by months.

Automated software testing has an entirely different cost structure. There’s an initial inversion plus periodical maintenance, but once tes automation is in place, we can run our tests as often as we need—for pennies.

With test automation, developers get continuous feedback, allowing them to spot problems very early in the production cycle. Quick iteration results in improved design, better quality, and safer launches.

Principles of test automation Entire books have been written exclusively on the topic of test automation. It’s a skill every developer needs to master at some point, and it’s better to do it sooner rather than later.

Here are six principles to ease the learning curve:

  1. Tests should improve quality.
  2. Tests should reduce the risk of introducing failures.
  3. Testing helps to understand the code.
  4. Tests must be easy to write.
  5. A test suite must be easy to run.
  6. A test suite should need minimal maintenance.

Principle 1: test automation improves quality Quality is an elusive concept. Try as we might, it’s impossible to define it numerically. Yet, we know it when we see it. The software industry has come up with many metrics to measure quality: number of defects, code coverage, CI error rate, test failure rate, and so on. Each one captures some aspect of the idea of quality.

Automated tests improve quality metrics by continually running hundreds or thousands of tests; finding defects before they reach production, informing developers of potential problems, and checking if the system deviates from user expectations.

Test reports in Semaphore shows a high-level view of a project’s state Metrics aside, we know that a solid design is a prerequisite for quality. When tests drive development, developers can easily try out different ideas and determine which one works best. This characteristic has been exploited to great success by practices such as Test-Driven Development (TDD) and Behavior-Driven Development (BDD).

Principle 2: test automation reduces risk Code review and peer programming, albeit necessary and productive, cannot be relied upon to find bugs. Experience shows that more eyeballs do not translate to fewer errors.

The only way to reliably find errors is to build a comprehensive automated test suite. Tests can check the whole application from top to bottom. They catch errors before they can do any harm, find regressions, and run the application on various devices and environments at a scale that is otherwise prohibitively expensive to attempt manually.

Even if everyone on the team was an exceptionally clever developer that somehow never made a mistake, third-party dependencies can still introduce errors and pose risks. Automated tests can scan every line of code in the project for errors and security issues.

Trivy scanning a project for security issues. Principle 3: tests help you understand the system Too frequently, developers return to code written only a few days ago only to realize they have completely forgotten how it works. This is even worse when developers have to deal with code written by other people.

Often, reading tests is the best place to understand a system, as they show how things work by example. So, when in doubt, developers can refer to the test suite.

Tests, for instance, can show another developer how an API should respond, allowing them to skip looking at the documentation.

ctx := context.Background() result, _, err := env.Client.Server.Create(ctx, ServerCreateOpts { Name: "test", ServerType: &ServerType{ID: 1}, Image: &Image{ID: 2}, SSHKeys: []*SSHKey{ {ID: 1}, {ID: 2}, }, }) ​ if err != nil { t.Fatalf("Server.Create failed: %s", err) } if result.Server == nil { t.Fatal("no server") } if result.Server.ID != 1 { t.Errorf("unexpected server ID: %v", result.Server.ID) } if result.RootPassword != "" { t.Errorf("expected no root password, got: %v", result.RootPassword) } if len(result.NextActions) != 1 || result.NextActions[0].ID != 2 { t.Errorf("unexpected next actions: %v", result.NextActions) }

Not sure if a line of code is necessary? Comment it out to see which test fails. Have an idea to improve a function? Need to refactor a piece of code? Try it out and run the automated tests. You’ll be surprised how much you can learn about a system from its tests.

Principle 4: automated tests should be easy to write Some tests start their lives as manual tests and get automated down the road. But, more often than not, this results in overcomplicated, slow, and awkward tests. The best results come when tests and code have a certain synergy. The act of writing a test nudges developers to produce more modular code, which in turn makes tests simpler and more granular.

Test simplicity is important because it’s not practical to write tests for tests. Code should also be straightforward to read and write. Otherwise, we risk introducing failures with the test themselves, leading to false positives and flakiness.

Many testing frameworks use Domain Specific Languages (DSLs) to define tests in plain English. Perhaps the most notable example is Gherkin, the language used by the Cucumber testing framework:

Feature: Is it Friday yet? Everybody wants to know when it's Friday ​ Scenario: Sunday isn't Friday Given today is Sunday When I ask whether it's Friday yet Then I should be told "Nope"

To sum up, it’s a good idea to stick to a few fundamentals when writing tests:

  • Write only one assertion per test.
  • Keep code separate from tests, i.e. production code should not include tests.
  • Keep tests independent from each other, as dependencies can quickly snowball into a headache-inducing mess.
  • Keep test overlap to the minimum, i.e. don’t test the same code twice.
  • Do not break the encapsulation of the tested code. Intead, only test external interfaces.

Principle 5: tests should be easy to run If a developer needs to open a checklist in order to start a test run, your tests won’t be run as often as they should be.

Ideally, tests would run every time code changes without any intervention. We’re in luck here, as developer tools are quite sophisticated. Most modern IDEs can detect changes in files and start the test suite automatically, and the same can be achieved with command-line programs like nodemon, live reload, fswatch, or testmon.

Caption: VS Code running tests in the background

For tests to be easy to run, some conditions must be fulfilled:

  • Idempotency: tests should not have side effects. Side effects include writing to files, saving to a database, or generally changing data. Developers should be able to safely run the same tests any number of times.
  • Deterministic: tests should always give the same result given the same inputs. When tests need external data that is out of the developer’s control, such as the date/time or a response from an API, these should be faked with mocks or stubs.
  • Independent: tests should be independent of each other, and developers must be able to run them in any order.
  • Lightweight: tests must be lightweight enough to run on the developer’s machine in a reasonable time.
  • Granular: developers must be able to run the test suite piecemeal.

Running tests on the developer’s machine is just part of the equation. Testing must also take place within your continuous integration pipeline. Your CI/CD pipeline acts as a quality gate; it runs the test suite on each commit, giving instant feedback and allowing developers to detect when a failure has been introduced.

Principle 6: an automated test suite should require low maintenance The last principle is a corollary of the previous five. That is, you get it for free if you fulfill the others well. Still, it is important, so it’s good to make a point of it.

Developers want to do creative and rewarding work. Automatization lets machines take care of the drudgery of testing. A positive feedback loop is created when tests are easy to write and are executed frequently. Developers tend to appreciate how automation makes their lives easier and, thus, are incentivized to write and maintain tests.

Some periodic maintenance will, of course, be needed to keep your tests in good shape. Here are four recommendations for writing and maintaining your test suite:

  • Write just enough tests to be effective (but not more). If errors are slipping by, you need more tests. Conversely, if you find that tests break with small changes, you need to remove some tests.
  • Choose the best type of test for the situation. Unit tests are fast and laser-focused, while end-to-end tests cover the UI and are heavy and more comprehensive. A test suite that follows the test pyramid has a healthy variety of tests.

The testing pyramid * Keep tests reliable. A test that fails when the code is correct is called a false positive. Tests that sometimes fail for no apparent reason are called flaky tests. Both cause problems in a test suite because they’re huge time-wasters and sources of frustration. * Keep tests fast. A slow test suite will put the brakes on development.

Conclusion Those who think testing is expensive are not fully aware of the cost of poor quality. Individually, the impact of bugs and defects on product value may be hard to measure, but can quickly spiral out of control if they are not addressed. Luckily, you can prevent this by building and refining your automated test suite, to serve as the foundation for a great developer experience and outstanding, quality software.

The post The 6 Principles of Test Automation appeared first on Semaphore.

View Details

In this podcast episode, we welcome Richard Kasperowski. Richard is an author, teacher, speaker, and coach focused on team building and high-performance teams.

Richard is the author of two books, High-Performance Teams: The Foundations and The Core Protocols: A Guide to Greatness, as well as the forthcoming book High-Performance Teams: Core Protocols for Psychological Safety and Emotional Intelligence.

We talked to Richard about what core protocols are, how important it is to talk about one’s feelings, and how to help your team achieve new heights. Listen to the full episode or read the transcript.

Things we covered:

  • How Richard started his career
  • What are core protocols?
  • Why psychological safety in teams is important
  • Why it’s important to share how you feel, even as a developer
  • How big should the team be?
  • What is mob programming
  • Things to do and not to do as a team leader

You can also get Semaphore Uncut on Apple Podcasts, Spotify, Google Podcasts, Stitcher, and more.

Like this episode? Be sure to leave a ⭐️⭐️⭐️⭐️⭐️ review on the podcast player of your choice and share it with your friends.

Introduction Darko Fabijan (00:02):
Hello, and welcome to Semaphore Uncut, a podcast for developers about building great products. Today, I’m excited to welcome Richard Kasperowski. Richard, thank you so much for joining us.

Richard Kasperowski (00:11):
Hey, it’s a pleasure. Thanks for having me.

Darko Fabijan (00:13):
Great. Can you just go ahead and introduce yourself?

Richard Kasperowski (00:16):
Sure. I’m an author, a teacher, a keynote speaker, a coach, a consultant. I focus on team building and high performance teams. I’ve written a couple of books about teams and have a great team and I teach a couple of university courses, actual software development at Harvard and an entrepreneurship course at Boston University.

Darko Fabijan (00:38):
Great. One of the things that you are known about among many things that you do are core protocols or, in terms that people would better understand it on this channel level is high performance teams.

Do we all want to work in high performance teams or have high performance teams? Of course. Yeah, I think that is not trivial. I think that each one of our listeners can identify with that part.

So yeah, if you maybe can give us an intro of how you ended up working on that team as one of the main things in your career.

How Richard starter his career in agile Richard Kasperowski (01:16):
I was a young software developer. I actually started pretty young my work career. I was 19 years old and I was a software tester, I was a software developer. I got really, really good at my individual skills.

I got to the point where I felt like I could do anything and because of my skills and abilities, I kept getting promoted. I kept getting these bigger jobs, bigger job titles, team lead, technical team lead, manager of teams. I got more interested from there into how we could be great together, not just me, but all of us because I was actually responsible for all of us being great together.

And that got me into things that today, people call agile. That got me into the agile technical practices drawn from extreme programming into the agile business practices, you might call them that, or social practices drawn from scrum.

And it got me into even more interesting things. It got me into thinking about the psychology of teams, organizational development, the emotional intelligence, learning more about myself, Myers-Briggs profiles and other kinds of personnel and personality profiling. And ultimately, somehow by chance, I get introduced to the work of the core protocols through Jim and Michelle McCarthy and realized, through a massive personal transformation, that these are a great set of tools for helping teams be their best.

What are core protocols? Darko Fabijan (02:43):
And for us who know little or nothing about the core protocols, what would be the essence maybe to start with and then we can dive deeper?

Richard Kasperowski (02:51):
Sure, sure. Just this idea of a great team, the high performance team and the jargon that people use in the research. Think back to the best team you’ve ever had in your life. Maybe you’ve been on more than one.

Imagine that you could watch a couple of high performing teams or five or 10 high performing teams and notice their behaviors and notice the behaviors that they have in common with each other. There’s probably commonalities between the best teams. There’s certain things they do that are similar to each other.

There’s probably commonalities between the best teams. There’s certain things they do that are similar to each other.

-Richard Kasperowski, Author, Coach, Keynote speaker

Our audience here is at least slightly technical. Imagine we could factor out the common behaviors and write them down as something that maybe other teams could learn or everybody in your team could reinforce if they were to do it, amplify and get more of it.

That’s what the core protocols are. They’re drawn from watching high performing teams, noticing their similarities, writing down these behaviors that they have in common with each other so that other people can learn them. So other teams can learn them and replicate those behaviors, putting names on those behaviors so we can talk about them more easily and building habits.

This is how we work together. And by building those habits together, we end up performing well.

Darko Fabijan (03:58):
Yeah. Clear. Sounds straightforward. However, yeah, we know in practice, as you mentioned, teams are usually composed of lots of different people, which is great, which is the value of the team and also very different ages and experience levels, often. Maybe what are some of the things that you are most frequently seeing and trying to help teams to implement and live, really, core protocols.

How psychological safety in teams is important Richard Kasperowski (04:26):
Some of the foundational habits or foundational behaviors to adopt, well, all of this goes back to the research. According to the research on teams and high performing teams, some of the most important things are psychological safety and group emotional intelligence.

Safety is this idea that when we’re together, we can take risks. It’s okay to try new things. It’s okay to throw an idea out there, even though you might not know that it’s an idea that the team will like, just toss it out there and see what happens. And you don’t get judged. You don’t get held back in your career. You don’t get threatened or fired in any way for not succeeding the first time at something. It’s okay to take risks. It doesn’t hold you back.

Richard Kasperowski (05:07):
And then, team emotional intelligence is this idea that we understand what we are doing together. We understand how we feel together and we can behave appropriately no matter how we’re feeling toward our goals, that we can modulate our behaviors individually and together as a group and we understand how things work outside of our team.

We’re probably operating in a larger organization or even in a small startup, there’s at least one boss or some stakeholders or customers outside of our team. We understand how things work outside of our team and we can influence others through our actions and what we say so that we can all accomplish our goals together.

Richard Kasperowski (05:46):
This is the research on teams and team performance and the research falls a little bit short in that it doesn’t tell you how to do that stuff. Core protocols offer a way, it’s a how-to to get this psychological safety and team emotional intelligence. Some foundations of these how-to, these actual behaviors are, first, everything is opt-in. We have freedom. We have autonomy. We do things because we choose to, not because anybody’s coercing us or forcing us to do things.

And you can opt out of anything anytime. So if you don’t want to do something with your team right now, we have a habit, a behavior, we just say, “I pass,” or, “I’m checking out.” And your teammates know what these phrases mean. “I pass,” is really just, “I’m still with you, but I’m opting out of this activity right now.” And it’s totally safe to do that. Nobody judges you or hassles you or tries to make you do it anyway.

Very concretely, this could be, it’s your turn at the daily scrum, but you’re not ready yet so you pass. Or it’s your turn to be the driver if you’re pair programming or mob programming, but you just not ready right now or you don’t want to do it right now, so you pass and it’s totally okay.

Why it’s important to share how you feel Richard Kasperowski (06:59):
This freedom foundation, this foundation of autonomy starts building safety together. We can opt out of things and it’s totally okay. You don’t get punished for it. And then, the second easy thing to do is share how you’re feeling. Now, this sounds wild to a lot of us engineering people. I grew up as a software developer.

Share how you feel? What? Just tell me what to do. Tell me what the work is. I’d rather write code. I’d rather think of algorithms. I’d rather figure out how to store a ton of data and optimize my code or whatever. Share feelings? No, I don’t want to do that, but it turns out, according to this science and research, okay, teams have to have technical competence, but all teams have technical competence, we’ll just be an average team.

If we want to be the best team, we add on to that with sharing how we feel with each other. We build up this team emotional intelligence.

Richard Kasperowski (07:52):
It adds on to our sensation of safety. It helps us perform better. The research is there and the easy habit is just say how you’re feeling to each other. “I feel sad about blah, blah, blah. I feel glad about blah, blah, blah.” In the background, there, this is an audio podcast so people can’t see what I see. You’ve got these nice drawings over on the side and I’m really happy about them.

Oh, maybe people can hear the sounds in the background. I actually love hearing the sounds of people’s kids in the background. It brings life into the work we’re doing. Wow, I just shared how I’m feeling about a couple of things. If we were working together, maybe you would do the same and we would start to connect more. It’s these teams in which people connect with each other, they’re cohesive. These are the teams that are better than all the rest.

Darko Fabijan (09:21):
This element of sharing the feelings, as you said, it can be scary for lot of people. You introduced here, the concept, us tech people.

Richard Kasperowski (09:32):
Yeah.

Darko Fabijan (09:34):
Guys and girls in the industry, it is a bit different crowd, not in a bad way.

Richard Kasperowski (09:41):
I self-selected into this group. When I was growing up, I felt safer by myself with a computer than I did with the other kids or the other young adults. It is a real growth step for me to be able to share how I’m feeling with people and it turns out we’re all better when we do that.

How big should the team be? Darko Fabijan (09:59):
Yeah. What I wanted to ask is, these elements of teams and team sizes and how teams are composed and so on, there is that element of holding any meeting and that right number of people who are in the meeting. And if you add literally one or two more people, everything breaks apart in a way. People start speaking less, just asking questions less.

So, yeah, I just connected that with my experience. And it definitely maps directly to there is less element of safety is then decreased with more people. And also, the element of sharing how you feel.

One of the big mistakes of my career, as the majority of people who a great career or no career at all is that they do progress from a developer to a tech lead or a manager and forward, that you put a group of 10 people and literally, everything becomes awkward. And then, if there is a smaller group, it works. And there are many advices in these areas of team of five is maybe ideal, or a couple.

Can you maybe share some of your experiences? Did you experience those situations where we gather maybe 10 people in the room and things collapse?

Richard Kasperowski (11:15):
Yeah. And one of the terms use for this is voice. Do you have voice? Is your voice being heard in the group? What we want, and again, back to the research teams that have more equal voice, and you can measure this by talk time. Teams that have more equal talk time amongst the members outperform other teams. I feel this.

When I’m more with a larger group, I’m a quieter person. So the larger the group, the less likely you are to hear my voice. And they’re loud people, the larger the group, the more likely you are to hear the loud people’s voices. I don’t mean loud as in the amplitude of their voice. I mean loud like they’re just hogging the talking time. They need their voice to be heard versus, I’m more of an introspective. I’m happy to just talk to myself even in the presence of others. Just listen to my voice in my head without even using my mouth.

Richard Kasperowski (12:08):
Yeah. This is an interesting podcast for the two of us, because we’re both inner conversers, inner thinkers. How do we get more voice? That’s a question.

In any group, one way, we mentioned this emotion check in idea, what if you start a meeting or start a work session with people sharing how they feel? I take a turn, you take a turn. The next person takes a turn. The next person takes a turn. Wow.

Right off the bat, everybody’s voice has been heard. We’re doing it, right there. It’s just how we start the meeting. We start the meeting by hearing everybody’s voice. We’ve all warmed up. We all know what’s going on with each other. We’re all connected more. The meeting will go better.

Richard Kasperowski (12:46):
And there isn’t a lot of research about team size. You mentioned the number five, that’s the number that most people quote. This research goes back to the early 1970s, about optimal team size for groups of people doing creative problem solving work. They really only measured two dimensions there. It was something like the quality of the outcome, self-reported quality of the outcome amongst team members in the group and self-reported satisfaction with having done of work together.

Five people is the optimal team size. We can all hear other’s voice and add on some intentional habits.

-Richard Kasperowski, Author, Keynote Speaker

And these two dimensions happen to intersect at the number 4.6, 4.6 people is the optimal team size. But of course, we can’t have 0.6 of a person so we round it up to five and say five is the optimal team size. Maybe small-ish groups work best for making sure we all feel comfortable with each other, we can all hear each other’s voice and add on some intentional habits. At least introduce yourselves, say your names, maybe share how you’re feeling. I’d say, yes, definitely share how you’re feeling and really connect right off the bat.

How to adopt new habits Darko Fabijan (13:48):
Yeah. Great. Great. And in this category of introducing these habits, I mean just those two small things, small, can make a huge difference, obviously, and I think that the majority of listeners would accept that statement. But in terms of introducing this, do you think that there should be a driver within the team, within the organization, or it’s individual things we are going to adopt this protocol?

Richard Kasperowski (14:15):
Good question. So how could we adopt these sorts of habits intentionally? If we don’t try anything intentional, we’re likely to have an average team. The vast majority of teams are average, but we all want to have a team that’s better than average. So how do we do that? Well, we have to try to have a team that’s better than average.

I’m doing a lot of piano to a playing and singing lately. I could just sit down at the piano and try a new song every day or not practice much and I would be an average pianist, but I’m doing things intentionally to be a better than average pianist. I’m doing a little bit of exercises every day, I’m learning songs that push me out of my comfort zone. I’m playing songs where I actually sound bad because I don’t know that technique yet, but I feel safe too.

I’m playing mostly by myself or my wife upstairs hears me. And it’s totally okay to try out these new things and intentionally build my skill.

Richard Kasperowski (15:08):
The same thing with a team. You’re going to have to try things that feel uncomfortable. You’re going to have to push your skills. If you want to be better than average, you’re going to have to intentionally do something different to be better than average.

A lot of teams do a retrospective. This is a really good habit, either daily improvements, continuous improvements or periodic improvements, this is a great habit for teams to have. But what if you don’t know what to do?

Richard Kasperowski (15:36):
We’ve identified some challenge or some growth point in ourselves as a team, but we don’t actually know what to do to solve it, to fix it or to get better at that growth area. It may help to have somebody who knows these skills, has read about them or heard about them or you just want to try them out together. You’ve discovered these core protocols or something similar and you want to try them out. Try them out just do it by the book. It’s thecoreprotocols.org. It’s free. You can just try these things out.

How people share skills and ideas Richard Kasperowski (16:04):
Sometimes it helps if somebody is already skilled at it. It could help to go through a class together. We run short classes on this. Or if somebody’s skilled at it, you can just start doing it. And people call this mirror neurons. If you start doing something, the other people in the group will imitate you. You’re nodding your head at me right now, so I’m nodding my head too. Or you’re smiling, so I’m smiling. It’s mirror neurons.

We imitate each other. It’s a human characteristic. If I start sharing how I feel, there’s a really good chance you will too. If I start using a particular way for us to make decisions together, there’s a really good chance you’re going to copy me. This is how humans work. This is how we learn from each other. This is how we share skills and ideas. So we could learn these things together or if you already know them, you can just start doing them and people will imitate you.

What is mob or ensemble programming? Darko Fabijan (17:38):
There are potentially some practices that could be complementary, potentially, to introducing this. I don’t know why, but mob programming keeps popping up in my head because it is one of those activities where you can feel unsafe and awkward, potentially, and you need to ramp up.

And what happens is that maybe someone is just, how you said, a louder voice, and then, can dominate the group or something like that. Can you maybe share your views or experiences around mob programming and how it helps?

Richard Kasperowski (18:16):
Yeah, mobbing or ensemble programming, people have different names for it these days, it is a great tool for getting more voice, for getting more creativity, for getting better solutions together, for connecting more, for even having fun together while we do the work, which is critically important. The practice in pairing or mobbing, one of the foundational practices is to periodically change who’s the driver.

We use this car driving metaphor. The driver in a car is the person at the steering wheel. You’ve got a navigator sitting next to you from the days before we had a, I’m holding up my phone in my hand, the days before the navigator looked like this little thing in my hand with maps and it talks to you and tells you what to do, the navigator was a person sitting next to you with a paper map unfolded in their lap, telling you what to do and where to go and giving you advice.

Richard Kasperowski (19:11):
But as driver, you’re actually executing the advice and you make the final decisions that whether you turn right at that street or at the next street. As a driver, you decide. The driver’s voice is heard the most within the code. The navigator’s voice, actual physical voice, might be heard the most in the group, but the driver’s decisions are final.

To get more equal voice in the team, we have this practice of changing who is the driver frequently, the person typing at the keyboard, controlling the mouse, typing the code. The more frequently we change drivers and the more drivers we have, the more equal voice.

And I guess, in this case, talk time means code writing time because we’re talking through code with each other while we’re building software and digital products. So changing drivers frequently is really important.

Richard Kasperowski (20:07):
Building habits with each other when we’re mob programming, we have many navigators, so building habits together to make sure that everybody’s voice is heard within the subgroup of navigators is really important. And pausing and reflecting on how to make things better, retrospecting our continuous improvement.

One of the interesting things about mobbing is, we get better results when we share our intention with each other.

-Richard Kasperowski, Author, Keynote Speaker

One of the interesting things about mobbing is, we get better results when we share our intention with each other, it’s not just you start writing code and the navigator starts telling me what code to write, but you say what you intend to happen before you start writing the code or what you intend to happen, what you hope the outcome will be before you start navigating.

I live in Boston. It’s not just, somebody starts navigating me to back out of my little tiny street to turn left to Maple Avenue, turn right at Church Street. It’s the intention. We’re going to drive to New York city.

Richard Kasperowski (21:05):
Knowing that intention, I can make lots of little micro adjustments without even hearing what the navigator might say as their detailed instructions. We just know what the goal is. Sharing the goal with each other is really important.

How to have an above average team? Richard Kasperowski (21:17):
A lot of the practices that people discover that work well for them mobbing together or ensemble programming together, they’re similar to or could be enhanced by some of the core protocols practices. What is your intention? What do you hope to get? This is one of the protocol’s passing. Everything is because you want to do it.

You don’t have to take a turn as driver if you don’t feel comfortable right now. Asking for help, this is another one of the core protocols. It turns out that the best teams, we ask each other for help and we typically end up helping each other. We don’t get stuck. This is something that we see in mob programming teams. We say we don’t know how to do something and somebody helps us. They navigate us through it.

Or we learn it together if it’s something that we’ve never done before. A lot of the things that happen with mobbing teams happen with any good high performing team, assuming people want to be really good, assuming you want to have a great team experience, assuming you want to build a really great product together.

It’s easy to have an average team. But to have an above average team, we have to intentionally try to be better than average.

-Richard Kasperowski, Author, Keynote Speaker

It’s easy to have an average team. But to have an above average team, we have to intentionally try to be better than average.

Darko Fabijan (22:21):
Yeah, absolutely. As you were talking about, this one thing pops up, experiences that I have, let’s say ensemble, I would take them back to when we were having some kind of outage or an incident is maybe a great example of how you can check how the team works in the pulse.

It’s not super high stress environment, but there is an element of pressure and timeliness in making decisions and also communicating and making mistakes has a higher impact.

Darko Fabijan (22:56):
However, ensuring that still, things work together fine. It’s not that I have a point. It’s more that it reminds me of that environment because there is usually a person who is a driver, explaining to us what happened than what we might do next and so on.

A question, as you’ve were describing your career and the progression that you eventually became a team lead and then entered some positions that include management and so on, over and over, I’m amazed how young our industry is and 50% of our developers writing code today have less than five years of experience or something like that.

For those people who feel confident with their skill and are attracted or pushed by organization to take leadership roles, what would be some of your advices? What are some of the most important things not to mess up?

Things to do and not to do as a team leader Richard Kasperowski (23:56):
Things to do, things not to do as you become a team leader. Figure out how to create this sense of safety together. That it’s okay to make mistakes. It’s okay to learn things. I’m not saying it’s okay to intentionally take down production. It’s not okay to intentionally cause your organization to lose money or move away from whatever your goal is. But sometimes, it happens.

So avoid accusation and blame and move toward asking questions. This was actually a really big growth point for me as a manager of other tech people. I got there because I actually did know the answers. I knew how to do things.

How do you write this code? How do you deploy that to production? Should we do it this way or that way? I got to leadership because I knew how to do these things. I knew the answers to these questions.

Richard Kasperowski (24:58):
I became a better leader when I let my people answer these questions themselves. Somebody might come to me and to ask a question, “How do I do this? How should I do this? I could do it this way or that way.”

And my answer would be more like, “What do you think? Let’s walk through it together. How could we decide? What would the positive outcomes be? What might the negative outcomes be? What are the consequences if we do it this way or that way?”

A big growth point for me and for our team was, let’s figure it out together.

-Richard Kasperowski, Author, Keynote Speaker

Or, two people come to me because they disagree about which path to take writing some code or building some components and they just can’t get aligned. They want me to decide, do it this way or do it that way.

A big growth point for me and for our team was, let’s figure it out together. I’m not going to decide. What would happen if we did it this way? What would happen if we do it that way? What are the best elements of both approaches? How can we combine the best of both approaches and make it a third way that’s even better?

Why it’s important to make decisions together Richard Kasperowski (25:59):
A side effect of the boss makes the decisions is, the people aren’t bought in on the solution and if they don’t like it, they can complain about the boss. And it’s like they don’t have personal responsibility. They don’t have a stake in it. They don’t have to give it their all. It’s the boss’s idea. “I don’t believe in this idea, but the boss told me to do it.”

But if we make the decisions together, it’s our decision and we’re in on it. Or if you make the decision by yourself, it’s your decision and you’re going to do everything you can to make it work. This is one of the ways that individuals and teams do better together. We are in together on our intention, on our goals and on the steps that it’s going to take to help us realize our goals.

We make the decisions together and we’re aligned with each other. So as a leader, as a new leader, take steps to make sure everybody’s voice is heard, that people are making decisions amongst themselves and you will have better results together as a team.

Darko Fabijan (26:55):
Yeah. I couldn’t agree more. It’s always pure gold. I wish that earlier in my career, I heard more of these things because most of the people discovered these things, it’s just that, how many mistakes you make, how many casualties you leave behind.

Richard Kasperowski (27:14):
For sure. I made plenty of mistakes.

Darko Fabijan (27:17):
Yeah, of course. That’s the best way to learn. Other quotes.

And for pepolpe who want to learn more, you mentioned the book, you mentioned a website about this, also research.

You’re also involved as an instructor on a number of courses. Can you give us a list? If we want to learn more, what are couple of resources to hunt for?

Richard Kasperowski (27:38):
Absolutely. For starters, just visit my website. It’s kasperowski.com. You’ll find a couple of my books there. You’ll find my blog there.

You’ll find my podcast there where I’m talking to people about the best team of their life and interviewing people. We’ve got 50 or 60 guests, 50 or 60 different opinions and life stories, experience reports about the best team of somebody’s life and what they did to make that happen. These are some great stories.

In addition to kasperowski.com, we’ve got thecoreprotocols.org, where you can find a really concise description of these behaviors. And on my website, you’ll find the books with more verbose descriptions of these behaviors and why they work.

  • High-Performance Teams: The Foundations
  • The Core Protocols: a Guide to Greatness

Darko Fabijan (28:22):
Great. Thank you so much. Good luck and yeah, thanks.

Richard Kasperowski (28:26):
All right. Thank you so much. This has been fun.

The post How to Build a High Performing Team with Richard Kasperowski appeared first on Semaphore.

View Details

Testing is a way to encourage change in a company.

Testing provides a robust safety net and supports code refactoring. Refactoring can refresh the codebase and make the code more flexible. Flexible software enables the quick introduction of new features, benefiting the business.

Testing, which appears to be just a mere technical detail on the surface, can energize the development of software products.

This article offers some suggestions to encourage the introduction of testing practices in companies looking to improve product quality and production efficiency.

Joining new companies and teams Before we start talking about testing, let’s take a look at a common situation that you might have experienced in your career.

When you’re new to an organization, you can’t always address existing technical issues. The possibilities vary depending on the role: an outside consultant can bring knowledge and experience but cannot, for example, make strategic decisions. A software developer working as an employee will have a different scope than a software architect or CTO.

In addition to role, technical issues are influenced by the available budget and the people who make up the workforce. For example, in some situations, development is outsourced to external software houses where intervention is not feasible.

All possible aspects aside, it’s always interesting to observe the dynamics that govern software production: every company has its own idiosyncrasies, and the best ideas are often drawn from them.

A pattern observed with some frequency is that of the young, dynamic, and responsive company, which over time acquires market share, growing vertiginously. As the company gets bigger, it grows sluggish and loses its initial momentum (as per the Ringelmann Effect). Satisfying customer needs becomes a very demanding process and delivery time increases. The rush leads to less time for reflection and improvement, resulting in a downward spiral: more and more defects, and the rework of existing features is continuous. Production and releases slow to a crawl and customer dissatisfaction grows.

There are many individual causes that give rise to such situations: over-demanding clients, under-staffed teams, lack of skills, and even more sensitive and personal issues in interpersonal relationships.

In practice, it is often a mix of several factors. An effective place to start is to take teams and companies through three essential steps: first, viewing; second, measuring; and third, improving. Before testing can be introduced and implemented, the current state of the organization (and its strengths/weaknesses) should be determined. This way, various stakeholders can be made aware of the current state of affairs and will be able to more easily see where testing can fit in and make major improvements.

Viewing In this context, the verb “view” is used in a broad sense. It starts with the actual visualization of data, then the introduction of physical and electronic boards to share the vision, roadmap, and backlog, and goes all the way down to information radiators that show the real-time health of the Continuous Integration pipeline. These activities lead people to observe behaviors and situations that are not clear to everyone. In the technical domain, it’s not uncommon to see teams gain a new awareness of the state of affairs in the organization and how their work relates to other domains.

Once we are “fully aware of our work and how it fits into the bigger picture“, it becomes natural to start measuring.

Measuring Without altering anything about the existing process, the second step is to introduce metrics. You could start by counting and cataloging activities, features developed, bugs introduced, support tickets processed. In a short time, patterns will emerge, and people involved in the process gain new insights. Personal feelings and judgments give way to data and statistics, which generally have the advantage of being more objective.

Improving Once you have identified the critical points, the first improvement experiments can be implemented. A clear hypothesis and a precise and measurable expected result allow for the beginning of the actual change.

What role does testing play in this design?

A vital role, as long as certain precautions are taken.

Testing as a catalyst for change In a previous article, we saw how there is a wide range of tools and approaches for testing.

In this stage, you can (and should!) engage multi-layered discussions regarding the available options and practices, in order to choose the best fit for the organization

With product teams and developers, it’s good to open a conversation to discuss current technical practices and tools, to share existing knowledge, and gain new insights.

Furthermore, it’s just as essential to foster a debate on the topic with staff outside of the development department. Non-technical staff interact with the product as well, and their expertise should be taken into account.

As we mentioned, testing is often relegated to a mere technical issue, but this is a poor practice. Testing hypotheses is an approach that has its roots in the experimental method: it is how science has distanced itself from superstition and morphed into its current form.

Therefore, testing and experimenting are some of the most effective ways to challenge our own assumptions and those of the people we work with.

When a customer is asking for a new feature, “How would you test it?” is a very powerful question that will get them to think analytically about their request: if their ideas are clear, it will be easy for them to offer plenty of test cases. If there is hesitation, the lack of detail is brought to light, and there is then an opportunity to further refine the requested functionality and the underlying needs. In practice, this can be done by taking advantage of Acceptance Testing benefits.

How to talk about testing to non-technical people One of the greatest obstacles to the introduction of cultural change is the inability to clearly express its advantages. Speaking of testing, it is not uncommon to find a certain inhibition or inability to convey the importance of a given change to people who are not directly involved with the technical side of things (or think they are not).

When addressing this topic with a team of developers, it’s usually pretty easy to reach a consensus about the benefits that testing brings to development (although there are plenty of naysayers).

However, there are prerequisites (time to learn new practices, introduce tools, etc.) and sometimes not everything is manageable within the team. It often happens that you are forced to ask for more budget (in time and money) to enable the desired change of pace.

And it is precisely when developers try to convey their needs to non-technical domains that difficulties begin to emerge. The reason for this is often that developers try to convince non-technical people by depicting only technical advantages, for example when talking about technical debt.

Developers often talk about refactoring and technical debt, and are unaware that non-developers don’t necessarily understand the context. Non-technical staff often think that adding new features simply boils down to adding more code: they don’t understand the complexity of the development process (as most developers don’t understand the inherent complexity of business affairs).

Very often, programmers fail to focus on the long red thread that leads testing through the entire implementation chain, from idea to software production, and consequently fail to convince people outside the team. It is important to frame discussions in a way that all participants can understand.

In these situations, the person who can help the team most is someone with a certain degree of reputation and authority, such as a coach, engineering manager, or CTO. The experience and the “super partes” position offer the possibility to bridge the proverbial gap from business to development, favoring the introduction of a “new language” everyone can understand.

A mature team, which has learned to see and measure the fruits of its labor, should be able to effectively communicate its experience to its business-oriented co-workers. This communication often takes the form of various data visualizations that display agreed-upon performance metrics. Information, in the form of metrics, is a powerful weapon to open the eyes of non-technical people to technical issues.

The number of open bugs, regressions, and time spent fixing existing code, compared to the time spent innovating and adding value to the product are all important issues to underline.

People leading teams and companies think in terms of tactics and strategies. A healthy company is one that makes a profit, by which it is able to maintain a certain level of well-being for its employees. When a development team can show that a new approach would lead to more efficiency and a better product, you can be sure that non-technical executives will be eager to implement it.

Testing culture plays a key role here: testing everything, from idea to production code, is one of the most effective ways to reduce waste and maximize efficiency.

The strategic importance of testing So, we’ve seen how testing has strategic relevance within the entire workflow of a company. Hopefully now we can stop thinking of testing simply as the way we “check that the software runs“.

Testing code is important, but it’s only one of the many places where we can use testing.

As we mentioned, testing should also become part of the culture of those who collect user needs. An effective business analyst doesn’t just collect requirements and pass them on to the development team; they are also able to explore each request, determining the actual need and getting the client to make the details clear and explicit.

Adding new functionality to a product without necessary governance can sneakily lead us into the “Swiss Army Knife” trap: little by little we create a tool with lots of functionality, but which doesn’t particularly excel at any of said functions.

The ultimate goal is to solve problems, developing software only when needed.

Less functional analysis, more examples In large firms, it is not uncommon to see an equally large hierarchy of roles and job titles. The communications between departments become complex, and with it so too does the manner of conveying the needs that should be met by the software.

In these situations, a typical solution is to use specification documents and meticulously collect requirements, which are able to accurately convey intent and leave no room for doubt or interpretation.

And the belief is that, once written, these requirements will never change.

Unfortunately, this often remains a mere illusion. Edward Berard once said that “Walking on water and developing software from a specification are easy if both are frozen”. The more widely the software is to be implemented, the more true this statement is.

This topic has been talked about for years. Brian Marick has spent rivers of ink emphasizing the importance of examples in testing, so much so that he named his consulting firm Exampler.

In a previous article, we brushed up on his work regarding tests, particularly their cataloging. In that important piece, Marick helps the reader understand the various types of tests, making an important distinction: it is one thing to verify results (checking), and another to explore possible unexpected behaviors (testing).

This distinction is of fundamental importance because, in addition to clarifying the types of tests and the techniques associated with them, it places the emphasis on what should be automated (checking) and what should be performed manually (testing).

When gathering requirements, it is therefore important to take these aspects into account: for functional requirements, there must be a large collection of tests (a test suite) that developers and QA engineers will implement via automated tests, which will be performed constantly in the CI/CD pipeline.

For non-functional requirements, we have to set up a suite of tests to assess the robustness and security of the solution offered; additionally, we need to design a series of exploratory tests that QA engineers can perform manually to constructively criticize the product.

The ability to ask for examples during requirement gathering, in addition to facilitating the writing of a test suite and the subsequent creation of automated tests, allows you to validate the clarity of the solution required by clients.

If the requester of a particular feature is unable to give clear examples of its use and is unable to accurately formulate the tests that would be required to verify proper implementation, then we have a clear signal that further refinement is needed before development starts. We will be able to separate clear requests from those that need further refinement to be implemented, user stories from spikes, and so on, all to the benefit of the development process.

Using tests as small contracts with stakeholders The concept of User Story was born in the XP days. Ron Jeffries (father of the User Story concept) introduced a key to interpreting the concept of a user story, the so-called 3Cs: Card, Conversation, Confirmation.

By Card Ron means the importance of having a place to gather information as it emerges. The term Card is not simply a symbol: the first user stories were actually written on index cards, which the early XP team found themselves having in abundance as the old filing cabinets fell into disuse.

The second “C” is the keystone: Conversation. Gathering and writing requirements are necessary activities, but often they are not sufficient. Those who ask for functionality must be able to explain their needs in a face-to-face conversation with the development team that will implement the required software. It’s only via this discussion that all doubts can be resolved, and the user will have the opportunity to better convey their needs, and that the programmers will be able to fulfill their key tasks: to question the expressed need, clarify ideas, highlight any technical implications, and finally accept the request and proceed to development.

The third “C” stands for Confirmation: once development is complete, a user story must be confirmed. Here is where the Acceptance Tests of XP culture come into play–where we, too, find our beloved tests. In this scenario, they act as validators of the initial hypotheses, almost as if they were the clauses of a small contract stipulated between those who ask for a functionality (the user or his representative) and the developers. And these clauses protect both parties: they allow those who requested the development to be sure that the system will behave as expected, and the developers to have a precise idea of when the development can be considered finished.

Promoting a culture of quality This article is the result of several studies and much practice in the field. We have not yet talked about quality: now is the time to do so.

What we have done so far is to describe the road that leads to the realization of a quality product, a road made safe and smooth by a multitude of tests, each with its own precise purpose. This is a fundamental prerequisite: one cannot proceed with determination on treacherous roads, where risks and dangers lurk around each corner. Just as we don’t drive too fast if road conditions are bad and we haven’t been to the mechanic in a long while. You can’t safely go fast if you don’t prepare well first.

Testing is a fundamental part of quality-driven culture, a prerequisite for avoiding the two biggest risks for a company developing products: the risk of doing the wrong thing, and the risk of doing it too late.

With this article, we wanted to bring the topic of testing to a higher level, try to dispel the myth that it is an engineering vanity, and convince you that testing’s rightful place in the business strategy.

Changing course is not a simple operation, many companies don’t make it to success. This article’s purpose is to provide encouragement to take the first steps and reduce the range of action and focus on a precise theme that is clear and able to influence every part of the company.

Tests have a cost, no doubt, but their costs vary enormously. The closer the tests are to those who make the product and the closer they are written and executed to the development cycle, the more effective and cheap they are. As time goes by, their cost increases: just think of the cognitive load of revisiting a theme from months or years earlier or having non-technical people dealing with unexpected anomalies and behaviors.

Waiving testing can be a way to save time and money, sure, but it can (and often will) come back to bite you down the road. There is no law that requires testing(excluding of course special cases subject to special laws), but that isn’t a reason not to implement tests.

Either way, if you don’t test your product, someone else will, and it’s more expensive to go back and fix problems with a finished product than it is to make fixes during development.

The post How to Introduce Testing in Teams and Companies appeared first on Semaphore.

View Details

From a management standpoint, Test-Driven Development (TDD) can be a difficult sell. It involves a relatively large upfront investment, has no obvious business benefits, and (most important of all) the customer is paying for functionality, not tests.

Tempting as it is to cut testing in order to reach the next milestone in the project, it is a mistake because, as we’ll see, TDD can help us accelerate development and reduce costs over a project’s lifecycle.

What is TDD? TDD is a set of principles for software development that uses testing as the catalyst for design. The TDD cycle consists of three phases:

  1. Write a test: the test defines the code’s behavior, the input, and the expected outputs.
  2. Implement: write the minimal code that makes the test pass.
  3. Refactor: improve the implementation, and make it more general, faster, or more readable. The test ensures that the implementation remains valid during this phase.

TDD is not a synonym for testing. You cannot simply add some tests to your project and say you’re doing TDD. For example, the old-school waterfall development method also uses tests, albeit at the end of the process, and is pretty much the antithesis of TDD.

The problem is that the testing phase comes too late. By the time tests are executed, too many design decisions have been made without grounding feedback. Serious issues revealed by tests implemented at this stage often send the project back to the drawing board.

TDD’s defining characteristic is using tests to drive design. This means that tests appear at the very beginning of the design process, before any code is written.

TDD creates a short test<->implement feedback loop that promotes fast iteration. It allows trying different designs and multiple implementations during development. This helps developers find optimal solutions in a timely manner, giving them confidence in their code and keeps the project on track.

According to a survey conducted by IEEE, TDD has several benefits:

  • Shorter time to market: TDD projects finished 30% faster on average.
  • Fewer defects: TDD produced code with fewer failures. Bug reduction was found to be between 40% and 80%.
  • Improved quality: some projects received an 80% bump in readability, understandability, and perceived code quality

Writing an automated test suite is an investment. It takes time, effort, money and a good degree of skill and experience. There are six principles that will guide us to write better tests:

  1. Tests should improve quality: tests should fail when bad quality code is checked in.
  2. Tests should reduce the risk of introducing failures: tests should be as simple as possible to avoid introducing errors in the test code itself.
  3. Testing helps to understand the code: it should always be possible to refer to the test case to understand what the code is supposed to do.
  4. Tests must be easy to write: we should choose an easy-to-use test framework and take the time to learn it well.
  5. A test suite must be easy to run: ideally, tests should run automatically without any intervention.
  6. A test suite should need minimal maintenance: tests should not be tied to the implementation details. This allows refactoring without breaking the tests.

How TDD reduces costs To put things in more concrete terms, we’ll define effort as the amount of work needed to implement a new feature, fix a bug, or change part of an application.

The effort is not constant; it changes as the codebase grows. In the waterfall method, the effort starts low and rises steadily. The problem is, like we saw earlier, that feedback comes too late to be helpful, so developers need to rely on guesswork and intuition.

TDD switches things around. Initially, the effort is high because we’re writing tests instead of implementing features. Additionally, we need to factor in the fact that switching to TDD takes a while to get the hang of.

Once you zoom out, however, TDD does not increase development costs. In fact, after a certain threshold, TDD becomes much more efficient than waterfall.

In a world where time is money, the initial cost of TDD is offset by the effort saved down the road. As a project advances, changing code gets easier because:

  • Tests save you a lot of debugging effort.
  • You have immediate feedback when something breaks.
  • Having a clear direction and a robust design results in more focused code.
  • Code tends to be cleaner, more decoupled, and thus easier to change.

TDD’s pitfalls TDD is not something that you can just plug into your development workflow. Frequently, TDD requires a cultural change, both in management and engineering teams.

Writing tests is a different skill from writing code and requires experience to master. Implementing TDD in a half-baked manner won’t get you the promised payoff for the initial investment. In fact, a poorly-written test suite will block development.

Management must set clear expectations and a coherent roadmap, ensuring that developers are on board and properly trained before attempting to adopt TDD. Failing to understand and properly apply TDD causes poor results. This, in turn, results in developers feeling like they’re wasting their time and general disappointment throughout the organization. These are a few signs that indicate when TDD is not going according to plan:

  • Teams are trying to reach 100% coverage.
  • A prevailing mentality that tests are only about finding bugs.
  • The test suite is not easy to run.
  • Not all the tests are automated.
  • The test suite runs slowly.
  • The CI/CD pipeline takes more than 10 minutes to run.
  • Tests are too tightly-coupled to the implementation.
  • A prevailing belief that TDD is a fire-and-forget solution (as opposed to an investment that needs periodic maintenance in order to keep tests fast).

Conclusion Getting started with TDD can be challenging, but it’s a game-changer. The increased speed and ROI cannot be compared with other development methods such as waterfall.

To continue on your testing journey, read these posts next:

  • Test-Driven Development (TDD): A Time-Tested Recipe for Quality Software
  • The Benefits of Acceptance Testing
  • How to Find and Eliminate Flaky Tests

Thanks for reading!

The post The Economics of TDD appeared first on Semaphore.

View Details

From developers to testers, from business analysts to management, everyone in your organization must be committed to keeping tests in top condition. If you have an extensive test suite, you’ll need a plan to focus the effort. Check out our 5-step framework for identifying and optimizing slow tests if you’re not sure where to start.

Software development is modulated by the tools supporting it. Of these, testing is the most widespread and has the largest impact. Keeping tests fast and responsive leads to improved productivity, better code quality, and higher deployment frequency.

When tests slow down, development follows suit. Teams get frustrated when they can’t meet their goals and organizations deploy as fast as they want. At Semaphore, we’ve seen our fair share of tests and have identified the nine factors that slow tests down.

How fast should tests be? Tests are consumed first and foremost by developers, who run them first on their machines before committing changes. Fast tests keep developers productive, enabling them to maintain the creative focus that’s so important for problem-solving.

By that measure, a test suite taking longer than the time it takes to stretch your legs and grab a cup of coffee is too slow.

Developers should be able to run tests locally — at least the part of the suite directly related to the code they’re working on. A speedy and easy-to-use test suite will encourage developers to run tests before, during, and after their changes.

Tests also affect the speed of CI/CD. The longer they take, the slower your pipeline is. This is important because the pipeline dictates how often you can release. A slow pipeline disrupts the development cycle.

To be more accurate, you’re not really doing continuous integration unless your pipeline takes less than 10 minutes. And because tests run in the pipeline, they should fit comfortably in that 10-minute window.

Making slow tests fast again How do we fix slow tests? And how do we accelerate a CI/CD pipeline? Here are the nine most common performance problems and their solutions:

  1. My tests are too large: break them up.
  2. My tests have multiple dependencies: isolate tests with stubbing or mocking.
  3. My tests are tightly-coupled: refactor them to make them independent.
  4. I have obsolete tests: remove dead code and outdated tests.
  5. My tests use sleep and wait: replace sleep statements with synchronization mechanisms.
  6. My tests use a database: make sure queries are optimized.
  7. My tests always go through the UI: reduce UI interactions. For example, if an API is available, test that instead.
  8. I set up my tests through the UI: use out-of-band channels for setting up and tearing down tests.
  9. My tests cover every edge case in the UI: focus only on the most critical paths for your users.

Breaking up large tests * Problem: large tests take a long time to complete, are hard to maintain, and cannot take advantage of concurrency. * Solution: divide et impera. Split the test suite into small units and configure parallel jobs in your pipeline.

Long, single-threaded tests do not benefit from the many cores modern machines come with. Let’s take a look at the following pseudocode, which tests one function in three different ways:

``` // tests 1 to 3 setup() value1 = function(input1) assert value1 == output1 cleanup()

setup() value2 = function(input2) assert value2 == output2 cleanup()

setup() value3 = function(input3) assert value3 == output3 cleanup() ```

Breaking this test up could make it up to 3 times faster. Depending on your testing solution, it might be as simple as distributing the code between three files and setting up your framework to use all the available CPUs.

| Test1 | Test2 | Test3 | | --- | --- | --- | | setup() | setup() | setup() | | value1 = function(input1) | value2 = function(input2) | value3 = function(input3) | | assert value1 == output1 | assert value2 == output2 | assert value3 == output3 | | cleanup() | cleanup() | cleanup() |

Parallelization, however, isn’t without dangers. Tests running in sequence do take longer but are easier to wrap one’s head around. Concurrency, on the other hand, is complex. It may produce unexpected results if tests have side effects and are not isolated. A small mistake can lead to hard-to-debug problems, race conditions, and flaky tests.

Parallel jobs in CI/CD If you take away only one thing in this whole article, let it be this: parallelization has the most immediate benefits with the least effort — mainly when applied to the CI/CD pipeline — despite the aforementioned hurdles in its implementation. Once parallelizing ceases to be cost-effective, continue with the rest of the recommendations in the article.

Dividing tests opens up a whole new level of optimization. Cloud services like Semaphore allow one to scale up continuous integration and delivery beyond one machine.

Make tests independently-runnable * Problem: tightly-coupled tests sabotage speed and make refactoring difficult. * Solution: decouple components so they are independently testable. Don’t break test encapsulation.

Designing testable code means building small, isolated, and decoupled components. We’ve seen that parallelization can help us reduce testing runtime. Separating components is a necessary part of this process.

Tests add value to the extent that they maintain their distance from the implementation. When they are too tightly-coupled , they break more easily and for the wrong reasons. A test should validate an external behavior, not how it works internally. Tests that are too coupled to the code hinder refactoring.

Imagine that we have a class representing a shopping cart, like the one shown below:

``` class ShoppingCart {

Item _lastItemAdded;
Item[] _cartContents;
float _totalSum;

function AddItem(Item) {
    _lastItemAdded = Item;
    _cartContents.Add(Item);
    _totalSum =+ Item.cost;
}

function getTotal() {
    return _totalSum;
}

function getItem(index) {
    return _cartContents[index];
}

}

```

And we write the following test:

``` // test item added item = new Item("bottle", 10.50); cart = new ShoppingCart(); cart.AddItem(item);

assert _totalSum == 10.50; assert _lastItemAdded.name == "bottle"; assert _cartContents.toString() == "bottle-10.50"; ```

The test knows too much about the internal details of the tested class. It’s breaking encapsulation, making it impossible to refactor the class without breaking the test:

  • The test checks on lastItemAdded instead of using the getter.
  • The test accesses private attributes like _cartContents.
  • The test depends on the manner in which objects are serialized.

Think about this: you want the test to fail only if the behavior of the public interface changes. So, let’s make tests all about the behavior.

``` // test item added item = new Item("bottle", 10.50); cart = new ShoppingCart(); cart.AddItem(item);

assert cart.getTotal() == 10.50; assert cart.getItem[0].name == "bottle"; assert cart.getItem[0].price == 10.50;

```

Branch by abstraction Large-scale refactoring doesn’t take place overnight — it’s going to take a few weeks or even months. Gradual change can be achieved using branching by abstraction. This technique allows you to continue shipping releases while change takes place.

Branch by abstraction starts by picking a component to decouple from the rest of the codebase:

Next, wrap the component in an abstraction layer and redirect all calls to the new layer.

The abstraction layer hides the implementation details. If the component is big and cumbersome, create a copy and work on it until it’s ready. Then, refactor the component until it’s independently testable.

Once refactoring is done, switch the abstraction layer to the new implementation.

Making tests self-contained * Problem: external dependencies slow down tests and add uncertainty to results. * Solution: replace external components with test doubles, stubs, and mocks. Remove services, databases, and APIs from the equation to make unit tests self-contained.

A mock is a simplified instance of a component that responds like the real thing, at least for the scope of the test in question. It doesn’t need to implement the full range of responses, only those deemed relevant. For example, instead of requesting actual data from an API endpoint like this:

``` function precipitation weather = fetch 'https://api.openweathermap.org/data' return weather.precipitation end function

function leave_the_house if precipitation() > 0 then return "Don't forget your umbrella. It's raining!" else return "Enjoy the sun." end if end function ```

We could replace the real precipitation function with a canned response. This would make the test self-contained and, thus, more reliable. This also avoid unintended abuse on the API. As long as the interface is maintained, the test code is still valid.

``` // Override the precipitation function function precipitation() return 10 end function

message = leave_the_house() assert message == "Don't forget your umbrella. It's raining!" ```

Of course, now that we’re decoupled from the actual API, any breaking changes will not be spotted by the test. We could implement a separate test that periodically polls the API and validates the result to mitigate risks. This type of test is called contract testing.

Mocking, stubbing, and test doubles allow you to run a piece of code in isolation. In the process, you make tests much faster because you are not constrained by the limitations of the environment. Without them, you get inconsistent results as your tests depend on external components that are likely out of your control.

To help you isolate tests, Semaphore enforces a clean environment for each job.

Remove obsolete tests and dead code * Problem: dead code and obsolete tests waste time and contribute to the project’s technical debt. * Solution: do some house cleaning. Remove all obsolete code from your project.

Don’t feel anxious about deleting code and tests. Tests get outdated when the tested code goes out of circulation. Obsolete tests are part of the technical debt a development team has to deal with sooner or later.

Obsolete tests are not necessarily the only ones that deserve deletion. Sometimes a test covers working code, like in the example below. However, if the test is trivial and doesn’t give much value, it shouldn’t be in your suite. In such cases, it is often better to remove it.

person = new Person() person.setName("John") name = person.getName() assert name == "John"

How to delete a test It’s safer to remove obsolete tests in two stages: 0. Run test suite locally and verify ALL PASS

  1. Remove the tested code.
  2. If nothing breaks, delete the test.

Tests should be falsifiable. In other words, removing the code should make the test fail — if it doesn’t, what were you testing?

Once we’re sure there wasn’t any collateral damage from deleting the code, let’s remove the test.

Eliminate wait/sleep statements from tests * Problem: wait statements slow down the test suite and hide a swath of design problems. * Solution: use proper synchronization mechanisms.

The presence of sleep statements in a test shows that the developer needed to wait for something and didn’t know how long. So, they set a fixed timer that made the test work.

Sleep statements are usually found between a function call and it’s verifying assertion:

output1 = my_async_function(parameters) // wait 2000 milliseconds wait(2000) assert(output1 == value1)

While this may work. It’s brittle and unpolished. Why wait two seconds? Because it worked on the developer’s machine? Instead of sleeping, you should use some kind of polling or synchronization mechanism to keep the wait to the minimum. In JavaScript, for instance, await/async solves the problem:

``` async function my_async_function() { // function definition }

output1 = await my_async_function() assert(output1 == value1) ```

Many languages also have callbacks to chain actions:

``` function callback(output) { assert(output == value) }

function my_function(callback) { // function definition

callback(output)

}

my_async_function(callback) ```

In both cases, the most correct implementation would also include some form of timeout to handle unresponsive endpoints.

Frequently, waits are added as a workaround for a flaky test — a test that succeeds and fails at times for no apparent reason. Waiting, however, is not a correct solution; it just hides the problem.

Waiting for services Sleep statements should be replaced with proper polling or messaging to determine when the needed resource is ready. This works, and in some cases it may be the only alternative you have:

``` while port not open sleep(100) end while

// test that needs port available... ```

But when you can, it’s much better to open the port and pass the test function as a callback instead. This will chain the actions without needing any explicit waits.

``` function run_test() // the port is open, run your test here end function

connect(port, run_test) ```

Optimize database queries in tests * Problem: suboptimal database queries waste resources and slow down tests. * Solution: profile your queries and optimize them.

When the test calls for a database query, first check if you can replace it with a prefilled dataset instead. The bulk of your unit tests should not depend on a database. It is inefficient and usually unnecessary.

How do you mock database calls? Imagine we want to count all users in a table. We have a function that connects to the database and issues a query.

``` import mysql

function countUsers(host, port, user, password) connection = new mysql(host, port, user, password) users = connection.exec("SELECT USERNAME FROM USERTABLE") return users.count() end function

print countUsers('localhost', 3306, 'dbuser', 'dbpassword') ```

While straightforward, it’s hard to mock. You would need to write a MySQL replacement that accepts the same parameters and behaves similarly enough to be useful. A better alternative is to use inversion of control. This pattern involves injecting code into the tested function to allow more control.

``` function countUsers(dbobject) if dbobject.connected is false throw "database not connected" end if

users = dbobject.exec("SELECT USERNAME FROM USERTABLE")
return users.count()

end function ```

Here we are sending a database object as a parameter, making the function call a bit more complex:

``` import mysql

connection = new mysql(host, port, user, password) print countUsers(connection) ```

Testing is more straightforward, however:

``` Class MockedDB // always connected connected = true

  // returns a canned answer, no db connection required
function exec
    return ["Mary", "Jane", "Smith"]
end function

end class

usercount = countUsers(MockedDB) assert usercount == 3 ```

Querying a real database Mocks don’t always work. You’ll find that an actual database is sometimes needed, especially during integration and end-to-end testing. Semaphore provides popular database services out-of-the-box that you can quickly spin up in your test environment.

When it comes to using a database, there are a number of classic slip-ups. Perhaps the most common is the N+1 problem, which stems from combining loops and SQL queries.

users = db.exec("SELECT id, name FROM Users") foreach user in users email = db.exec("SELECT email FROM Emails WHERE userid = $user['id']") body = "Hello $user['name']" sendEmail(body, email) end foreach

What’s the problem with this? At first glance, there’s nothing wrong: get all users from the Users table, then their email address from a different table. But, think of how many queries are hitting the database: one to get all users and one more for every user found. If you have 10,000 users, that’s 10,001 queries.

Databases, SQL databases in particular, are designed to work in sets. They hate iterations because every query has a large upfront processing cost. We can reduce the 10,001 queries to just 1 by requesting all the records in one go and taking advantage of the JOIN clause:

users = db.exec("SELECT id, name, email FROM Users JOIN Emails ON Users.id = Emails.userid") foreach user in users body = "Hello $user['name']" sendEmail(body, $user['email']) end foreach

Selecting all columns

Selecting all columns is another easy-to-make mistake. The query SELECT * presents several problems:

  • Retrieves unneeded columns, causing more I/O and memory usage.
  • Negates some of the benefits provided by existing indexes.
  • Breaks more easily if column order or column name changes when used in JOINs.

So, instead of selecting all columns:

SELECT * FROM Users U JOIN Orders O on O.userid = O.id

You should be more explicit and ask only for the data you need:

SELECT U.id, U.name, O.shipping_address FROM Users U JOIN Orders O on O.userid = O.id

Batching operations Batching is the ability to change multiple records in a single transaction. Reaching data speeds much higher than regular transactions, batching is an excellent way of initializing a test. Thus, instead of inserting one row at a time like this:

INSERT INTO founders (name, surname) values ('John', 'Romero'); INSERT INTO founders (name, surname) values ('John', 'Carmack'); INSERT INTO founders (name, surname) values ('Tom', 'Hall');

Consider bundling the values into a single statement:

INSERT INTO founders (name, surname) values ('John', 'Romero'), ('John', 'Carmack'), ('Tom', 'Hall');

Similarly useful devices exist when setting up a test through an ORM. Take this RSpec example:

``` before(:each) do @user = User.create(name: "Adrian", surname: "Carmack") end

after(:each) do @user.destroy end

context 'model test' do subject { @user } it { should validate_presence_of(:name) } it { should validate_presence_of(:surname) } it { should have_many(:projects) } end ```

It takes a bit of knowledge of Ruby to understand that before(:each) and after(:each) are not the optimal choices here. The test creates and deletes a user for every it statement. Since none of the tests mutate data, you can initialize the dataset once with before(:all) and after(all), which run only once at the beginning and end of the test. Consider loading up the sample data once and reusing it in your tests whenever possible.

Test the API instead of the UI * Problem: too many UI tests reduce the performance of the suite. * Solution: avoid the UI when there are better testable targets such as an API endpoint.

An API is designed for programmatic access and it will always be a better fit for running tests than the UI. This doesn’t mean that the UI should not be tested, only that if you have the choice of testing via API or via UI, the former is easier and less resource-intensive.

The following example shows a basic UI test. It visits a URL, does a search, and verifies the resulting value:

``` driver = new ChromeDriver(); driver.get("http://myapp/users");

// search for a user driver.findElement(By.linkText("User")).sendKeys("John"); driver.findElement(By.linkText("Search")).click();

// validate result firstTableCell = driver.findElement(By.Xpath("//table/tbody/tr[0]/td[0]")); assertEquals(firstTableCell.getText(), "John") ```

Even such a simple test requires a lot: we need to open a browser, wait for the page to render, and simulate all the interactions. Not to mention that the entire application must be up and running before we even begin.

Compare that with a more subcutaneous test pinging the same API endpoint that the application consumes.

``` request = fetch("http://myapi/v1/users$?name=John");

assertEquals(request.status(), 200); assertEquals(request.header.contentType, "application/json"); assertEquals(request.body.users.name, "John"); ```

Here we’re testing that the API behaves according to the spec, and achieving the same thing as the test that went through the UI. This kind of test scales up much better because it uses fewer resources and can be more easily parallelized.

Reduce UI interactions during setup * Problem: setting up tests using the UI is suboptimal. * Solution: prepare the test out-of-band and only go through the UI for elements under test.

Since UI tests take more time to run and more effort to maintain, we need to keep them lightweight. If you want to test the checkout button, test the checkout button and nothing else. You don’t need the UI to fill the shopping cart with products. You can do it behind the scenes by populating the database or directly calling an API.

Of course, you still need to do end-to-end testing, and it is more approachable with a BDD framework such as Cucumber or JBehave. Test cases can be written down using Gherkin’s structured Given-When-Then pattern, which helps to make them more streamlined:

``` Feature: Checkout cart User must be able to buy their products

Scenario: Successful purchase Given cart has products When I press the checkout button Then my order should be accepted ```

Cheating in the test

The scenario above calls for a shopping cart with products. This is a given precondition, but the test doesn’t care how the cart was filled. We can “cheat” and prefill the cart directly with a database query, which means that we don’t have to go through the UI. The point here is that not everything needs to go through the UI.

@Given("cart has products") public void cart_has_products() { stmt.executeUpdate("INSERT INTO Cart (user_id,product_id) VALUES (1,1000), (1,2000), (1, 3000)"); }

In comparison, both the When and Then clauses need to deal with the UI since they are testing it in order to validate the checkout experience. Under the hood, each clause in the test is implemented using a framework such as Selenium:

``` public class StepDefinitions {

private WebDriver driver = new ChromeDriver();

@When("I press the checkout button")
public void i_click_checkout() {
    driver.findElement(By.linkText("Checkout")).click();
}

@Then("my order should be accepted")
public void i_should_get_order_number() {
    assertNotNull(driver.findElement(By.xpath("//*[matches(@id, 'Your order number is \d+')]")));
}

} ```

Testing frameworks have ways of reusing scenarios and conditions. Cucumber, for instance, has backgrounds. Time spent optimizing UI interactions is usually time well-spent.

Keep UI tests focused on the happy paths * Problem: trying to test every corner of the UI. * Solution: be clever about which cases to test. You don’t need to try every possible combination.

By now, you may have realized that the motto for UI and end-to-end testing is “keep it short, keep it valuable”. Each test should earn its keep.

Being too strict about the UI is not a good idea, as it makes it easy to break a test by moving a button or changing a string. You should be able to make cosmetic changes and improvements without breaking your end-to-end test layer.

That doesn’t mean that UI tests aren’t valuable. We just need to be picky about which paths to test. As an example, you most definitely want to ensure that new users can create an account or that existing users can log in. Conversely, testing what happens when a user enters a non-Unicode character into a search box is better served by a unit test.

There are no hard and fast rules. Finding what works depends entirely on the nature of your application. Think about the primary user experience (the happy paths) and forget about edge cases — at least where UI testing is concerned.

Don’t neglect your tests The importance of responsive tests cannot be overstated. Tests are code and should be treated with the same care. Time must be allocated to upkeep them because the speed of the test suite directly correlates to how often you can release software. A test suite will gradually slow down unless it is dutifully maintained, dragging the team’s morale down and making your organization miss deadlines. Don’t let that happen!

The post 9 Ways To Make Slow Tests Faster appeared first on Semaphore.

View Details

In this episode of Semaphore Uncut, we welcome Aslak Hellesøy, Cucumber Founder, Cucumber Open Source Lead at SmartBear. We walk about TDD and BDD, how to adopt automated testing, what different kinds of tests there are, and what the future holds for Cucumber.

What we talked about, in more detail:

  • Introduction
  • The differences between TDD and BDD
  • How developer communities evolve
  • What is Cucumber?
  • How companies started adopting Cucumber
  • TDD: patterns and anti-patterns
  • How to adopt test automation
  • Hexagonal architecture of BDD
  • Why do we need different kinds of tests?
  • The future of Cucumber

Listen to the full conversation or read the edited transcript.

You can also get Semaphore Uncut on Apple Podcasts, Spotify, Google Podcasts, Stitcher, and more.

Like this episode? Be sure to leave a ⭐️⭐️⭐️⭐️⭐️ review on the podcast player of your choice and share it with your friends.

Introduction Darko Fabijan:
Hello, and welcome to Semaphore Uncut, a podcast for developers about building great products. Today I’m excited to welcome Aslak Hellesøy. Aslak, thank you so much for joining us. Please introduce yourself.

Aslak Hellesøy:
Hi Darko, thanks for having me!

I’m a programmer. I’ve been a programmer since the late 90s, so that’s over 20 years now. I’ve been programming in lots of different languages. Started with Java, then did Ruby for many years, and some .NET, and lately Go, JavaScript, TypeScript, and probably a few others.

I’ve been an open-source contributor. I started doing that quite early around 2003, when I released some tools in the Java space that nobody uses anymore, and then moved over to work on the RSpec project. Cucumber grew out of RSpec. I’ve been working on Cucumber since 2008, 2009, so that’s about 13 years.

I’m also an entrepreneur. I founded a company in 2013 or ’14 that we sold in 2019 to a company called SmartBear, and that’s where I’m currently working.

Darko Fabijan:
Great. Thanks. And our ways cross in a way that I was introduced with a team to TDD, BDD, Ruby, and all that through a Ruby community. It was one of the early adopters of TDD and BDD and was pretty religious about it.

Then Cucumber is one of the main building blocks of BDD. Can you please give us a short introduction to how you came up to create Cucumber and what your take on the BDD is?

Aslak Hellesøy (02:05):
I think we actually have to go nearly 20 years back in time to 2003. At the time, I was living in London and there was a community of extreme programming or XP practitioners who met up regularly. I was working in a company called ThoughtWorks at the time. And TDD being part of XP was something that everybody was very interested in.

One of my colleagues at the time, Dan North, was training a bunch of people in TDD. He realized that TDD was really hard for people to grasp. He ran into this frequent misunderstanding that people thought it was about testing, whereas the way he and many others thought of it is not really about testing, it’s just a development workflow.

So he came up with BDD as a variation of TDD to make it easier for people to understand the core values of TDD.

The differences between TDD and BDD Aslak Hellesøy (03:02):
The main difference between TDD and BDD is that BDD has a stronger emphasis or collaboration between technical people and business stakeholders. So it’s kind of a tool to discover misunderstandings that happen very early on in the requirements phase.

The main difference between TDD and BDD is that BDD has a stronger emphasis or collaboration between technical people and business stakeholders

-Aslak Hellesøy, Founder of Cucumber.io

And the other thing is expressing the understanding of what the requirements are, not as unit tests, which is what you would do with traditional TDD, but rather as acceptance tests. So higher-level tests that are expressed in a way that the technical stakeholders can understand.

Aslak Hellesøy (03:38):
I just want to emphasize that both of these things – collaborating with the non-technical stakeholders and using acceptance tests – were actually emphasized originally by TDD. But it was sort of not regarded by the community as the most important thing. So BDD tries to take those two values as two of the most important aspects of the practice.

So around this time, JUnit was pretty mature already, and there was RSpec, NUnit, and lots of unit testing tools. But apart from FitNesse, there were no other tools for writing acceptance tests, using a syntax that can be read, and understood, and even written by non-technical people.

So that’s why I started Cucumber. I wanted to write these kinds of high-level, readable, acceptance tests that everybody could understand, so I needed a tool. It started out in RSpec, and there was later extracted into its separate tool.

How developer communities evolve Darko Fabijan (05:07):
You spent a lot of time as part of these communities. Let’s say we can put them on their own single umbrella. What are maybe some changes that you have seen?

Because these things can have adoption cycles, which is very funny. In a way, there are different communities usually form along with the languages or frameworks, and then they evolve and then embrace various tools at different rates, and with a different level of engagement. So we cannot say it’s 20 years, but it’s also not too far away from that.

Have you seen any changes, any patterns in adoption how teams are embracing? They’re also a completely new generation of people. Every three to five years, there is a new generation of developers.

Aslak Hellesøy (06:04):
Yeah, definitely. So the first release of Cucumber came out in 2008… Or was it ‘9? I can’t even remember. And at that time, it was only Ruby.

It came out at a time that Ruby on Rails was quite new. So the first adopters of Cucumber and also BDD were Ruby on Rails developers. At the time, there were also some tools in the Java space, but primarily, all of the buzz around it came from the Ruby community.

Two or three years later when my friend, Matt Wynne and I had realized that Cucumber was kind of taking off, we decided to write a book about it. So when that book came out three years into Cucumber’s life, it made it more accessible to a whole new audience.

The Cucumber Book by Aslak Hellesøy and Matt Wynne The people who picked it up then were testers. People who maybe traditionally had been doing a lot of manual testing were now adopting Cucumber as a way to automate their tests.

Aslak Hellesøy (07:03):
I think that’s when we see the first shift in paradigm, because, as I alluded to earlier, TDD and BDD aren’t about testing. It’s really a programmer-focused workflow, at least that’s the way we intended it to be, and I guess in many ways still intended to be.

But people have adopted Cucumber as an acceptance testing tool where they write the tests afterwards, rather than before they write the code. At the same time, people would start using Cucumber with browser automation tools like Selenium WebDriver, and Watir, and other things. It started out as being a tool where developers would use it, not necessarily to test through the UI, and almost overnight, it became a tool that was used by testers who would use a browser automation framework to test existing code.

TDD and BDD aren’t about testing. It’s a programmer-focused workflow.

-Aslak Hellesøy, Cucumber Founder

Aslak Hellesøy (07:53):
So that happened around 2012 or something like that. I would say it hasn’t really changed much in the decade that followed after that. But 2022 and the majority of Cucumber users are still using it as a test-after tool with a browser automation tool.

We’ve tried to educate our users and the community about other ways and more efficient ways to use it. But I think we’ve failed in that regard, even if we’ve been standing on the rooftops, shouting it really loud.

There’s just so much, I would almost call it, misinformation out there about using it as a testing tool that that is what most people are using it as.

What is Cucumber? Darko Fabijan (08:35):
Just a couple of months ago, I was speaking with one of our prospects. They said, “Okay, we are really struggling with connecting our UX teams and business people or developers. We are separated by silos and separated by brick walls. We have found this great tool, Cucumber. I’m really happy about that.” Look, it’s been around for a decade.

Aslak Hellesøy (09:02):
Well, it’s a funny thing because Cucumber is a decent testing tool, but it’s just that. It’s a tool that, if you use it right, you can get so much more value from it.

So I don’t really mind when people use it with the web driver and just drive UIs with it. I just feel a bit sorry if they haven’t discovered the BDD way of using it.

But you can only really do that if you are in control over the whole stack, which is difficult when you’re working in a silo, either as a front-end developer who doesn’t touch the backend or as a tester who just doesn’t touch anything, just tests the front product.

How companies started adopting Cucumber Darko Fabijan (09:35):
I was interested to hear about your efforts to try to teach people, and to sway the whole community in that direction, and what are some of the experiences and how it looked.

Aslak Hellesøy (09:49):
When I started Cucumber Limited with some of the contributors, one of the ways that we made money was to do trainings for companies. So a company would come to us and say, “We want to adopt. We’ve heard about all the benefits of BDD and Cucumber. We want to adopt it. Can you help us?”

So we would run a three-day training course with them, and teach them how we think it ought to be used. And then occasionally, we would also get coaching engagements where we would come back and spend some time with them after the training and just help them as they were learning and putting into practice what they learned in the training.

So I guess that’s probably the most significant effort that I and my colleagues have done. We’ve also written blog posts, and we’ve had podcasts, and Twitterstorms, and so on. But there’s only so much you can do to educate when there is so much more additional information out there that says something different.

Aslak Hellesøy (10:54):
Lately, there’s a new book series called the BDD Books, written by Seb Rose and Gaspar Nagy, that teach you the three main practices of BDD as we’ve come to understand them, which is Discovery, which is a completely non-technical thing. It’s just, “How do you have efficient communication between the development team and the non-technical stakeholders?”

And the second one is Formulation, “How do you capture and write down in Gherkin documents in a good way?” This understanding from the Discovery, and then finally Automation, “What are some good techniques you can use to automate this stuff?”

So two of these books have come up, Discovery and Formulation, and the Automation book is in the works. Those three books are capturing, I would say, all of the knowledge that we’ve sort of built and evolved as a community over the past decade. So for anybody who really wants to learn BDD, I would recommend those three books, bddbooks.com.

TDD: patterns and anti-patterns Darko Fabijan (11:57):
That’s a great resource. We’ll make sure to include it in the notes. In the beginning, there was RSpec book and the Cucumber book. If you are a Ruby developer, those are like Bibles, and you started from there, and you had to go…

I remember for me, and also some of the colleagues, it was a bit of a struggle that you are embracing the practice of TDD and BDD at the same time, which are on different levels. It’s not trivial because, to the best of my knowledge, still the majority of the universities in computer science courses are taking, generally, design and testing overall as some afterthought. It’s not the first-class citizen on the courses.

With your experience of teaching teams and helping them embrace BDD and Cucumber, do you also have connecting points with TDD in those companies as a design tool? What are some patterns and anti-patterns that you have seen? What’s hard for people to grasp, what do they usually miss?

Aslak Hellesøy (13:01):
One thing that’s hard to grasp is the same thing that was hard to grasp for the people that Dan North was training when he came up with BDD, which is realizing that BDD and TDD are not about testing existing code, it’s about designing a code that hasn’t yet been written.

BDD and TDD are not about testing existing code. They are about designing a code that hasn’t yet been written.

-Aslak Hellesøy, Founder of Cucumber.io

That is really hard for a lot of people to grasp, especially non-developers because you have to be a developer or a programmer to understand those concepts.

So when we were teaching people, we would teach everything from product owners and business analysts to manual testers, to developers. But quite often in enterprise organizations where you have silos, there can be barriers in the organizational structure that make it really hard.

Even if you understand BDD, you’d have to put it in place because you need to tear down some of those walls that prevent people from collaborating efficiently.

Aslak Hellesøy (13:54):
Another anti-pattern I’m often seeing is when a team or an organization realizes that all of the manual tests that they currently rely on are too slow, and too inefficient, preventing them from delivering continuously. They want to basically translate all of the manual tests into automated tests, and they have these long manual tests, and they’re starting this journey to automate them.

That can become a real struggle because you’re dealing with a system that wasn’t really designed for test automation. One of the common problems is that you can’t easily put the system in the state that you wanted to be in order to run those assertions that you want, simply because it’s just a black box.

How to adopt test automation Aslak Hellesøy (14:40):
So what I usually recommend to those teams is that it’s good that you want to adopt test automation, but do it very incrementally. Only do it when you’re adding a new feature to the system, for example. Do test automation for that new thing, but don’t worry too much about the old stuff. You can still use your manual testing effort for that.

It’s good to adopt automated testing but do it very incrementally.

-Aslak Hellesøy, Founder of Cucumber.io

And the other thing is, if you have tests that never fail, there’s no point in automating those. You should focus on the ones that tend to fail often, or the ones that verify very critical behavior in your system.

So there are a lot of techniques you can use to filter out from those 10,000 manual tests you have, and maybe filter it down to 50, and focus on those. Every time you modify something, write an automated test. So you have to adopt this stuff incrementally, you can’t have a big bang translation effort.

Every time you modify something, write an automated test.

-Aslak Hellesøy, Founder of Cucumber.io

Darko Fabijan (16:12):
I remember some more experienced colleagues saying to me about the piece of code that I wrote, “Okay, code looks decent, but how we’re going to test it?” It involved connecting to some payment gateways, and APIs, and all of that, and that cannot be easily tested.

So that’s one of the moments when I realized, “Okay, I now start to get why BDD and TDD are also design tools, and you design software through them because you must enable yourself to test the things that you wrote”.

Aslak Hellesøy (16:45):
There’s one question that every tester and developer needs to ask themselves when they design a test, and that is, “What kind of confidence do I want to get from this test? Do I want to get confidence that the business logic is working correctly?” That’s one kind of confidence.

“Or do I want to get confidence that it works on this particular platform? Does it work in this particular browser? Does it work on this particular phone?” Well, that’s different confidence.

The third kind of confidence is, “Does it interface correctly with these external systems?”

You can come up with different kinds of confidence categories, performance, business logic, interacting with external systems, but if you’re trying to get confidence that all of these things are working in the same test, that’s not going to work. You have to design tests specifically for a particular kind of confidence.

You have to design tests specifically for a particular kind of confidence.

-Aslak Hellesøy, Founder of Cucumber

Aslak Hellesøy (17:42):
So we need a lot of confidence from our systems. We need to make sure that they work on different platforms. We need to make sure that the business logic is implemented correctly. We need to make sure that interaction with external systems is working well.

You can’t get all of that confidence from the same tests. You need different tests to test on platforms. You need different tests to test business logic. You need different tests to test interaction with external systems.

That’s one mistake that a lot of people are making, they try to fit it all into the same test. So when I do BDD, I tend to focus on the business logic aspect. I want to make sure that the business logic is correct, that’s really where BDD shines.

Aslak Hellesøy (18:23):
It’s not a great way to verify that you’ve integrated properly with a payment gateway. It’s not a great way to verify that this system works on all these different operating systems, or browsers, or mobile phones or whatever. You can use other kinds of tests for that.

So if we accept then that BDD is great for making sure you have implemented the business logic correctly, that means you need to be able to decouple all of the external factors that can pollute your result, for example, a payment gateway or something that can slow it.

So anything that’s slow or unreliable, you want to decouple that when you run your tests because it’s important that your tests are really fast, and it’s important that they all produce the same result.

Hexagonal architecture of BDD Aslak Hellesøy (19:10):
So the technique that I use, and many other people in the BDD community use, is something called ports and adapters, or hexagonal architecture. You put an interface. It’s probably easier to explain this with a statically typed language, like Java.

So rather than your business logic invoking, calling out to some sort of payment API directly, you define an interface that encapsulates, that sits in front of that payment gateway, and then you can have one implementation that actually talks to the payment gateway and another one which doesn’t do anything. Maybe it hasn’t prerecorded results or something like a stub.

And then you can still test your business logic and verify that it behaves correctly without connecting the real payment gateway. And then you have another test that will verify that your payment gateway interaction code is working correctly, but that would be a unit test that you run as an integration test. So we need all these different kinds of tests. We can’t do everything with the same tests.

Why do we need different kinds of tests? Darko Fabijan (20:11):
Very well explained, very valuable. But you’ve probably seen that, as you mentioned the Rails community, it’s so easy to couple that verifying the business logic is okay with the actual implementation of the platform, which is usually web and some web interface in the middle. I’m guilty of doing that many, many times throughout my career.

What I’m seeing these days with customers, there is that very expensive test suite that ends up being maintained, which is testing things all the way. A small change, a small feature added is actually testing almost the whole browser, the whole JavaScript test, all the way down to the database with various networking calls all the way back, being very fragile, very expensive to run over and over.

I’m seeing a lot of teams struggling with that testing pyramid and how they maintain it. Different people in teams employ different kinds of iterations, and try to heal that, or just steer it in the right direction. Any tips or advice that you can give in that area?

Aslak Hellesøy (21:28):
You do need some of those tests. You do need some of the tests that go through the entire stack, those are your smoke tests. Those are the tests that verify that everything is working together.

Those are the tests that try to give you all the kinds of confidence, confidence that your database is fine, that your external APIs are fine, that it works in this particular browser and your business logic. It’s good to have a few of those.

But for a medium-sized system, typically I wouldn’t have more than maybe a dozen of those. Maybe each one of those tests run in five to 10 seconds, and you can run them in CI, you don’t need to run them on your workstation every time.

But then there’s everything in between. I think that’s where you should be using unit tests.

Testing as car manufacturing Aslak Hellesøy (22:10):
Think of the hardware manufacturing industry or even car manufacturing. When something comes off the conveyor belt as a finished product, when you’re making a new car… I don’t know how they test cars, but they probably drive it around the block just to see if it kind of works.

But there’s been so much testing going into the individual components before that, everything from the software inside the car to every different physical component that they’re tested in isolation.

But the reason why that testing doesn’t require another very thorough, full-stack testing afterward is that they have some well-defined interfaces. They have some well-defined interfaces between the various components so that when you put them together, you don’t need to do thorough testing.

This, again, is related to ports and adapters. When you’re designing an application according to ports and adapters, you are defining what those interfaces are, and you can test the components at their interface, and you don’t really need to worry so much about testing it again when you bundle it together.

Mixing concerns in the same code Aslak Hellesøy (23:12):
I do think that statically-typed languages help a lot in this regard because you can have the compiler verifying that you are respecting the interface or the contract between components.

But with dynamically-typed languages, there is no compiler that will tell you if you’re calling a method that wants three arguments with two arguments. You’re not going to know that at compile time because there is no compile time. So with languages like Ruby, unfortunately, you do need to rely more on those tests that test a whole assembly.

And then there’s also the thing about what you mentioned, you’re guilty, and I think everybody who’s built a Rails application is a bit guilty of mixing concerns in the same code.

For example, putting the business logic in your active record models makes it really hard to test that business logic without a database, which makes the whole thing really slow and so on.

But that’s just the way Rails is designed. That’s the price you’re paying for the ridiculously rapid development experience you get, at least at the beginning of the project.

Applying a more functional programming paradigm Darko Fabijan (24:09):
Yeah, yeah. Exactly. I’m mostly speaking with people who have 10-year-old Rails apps. Those rapid things, in the beginning, are biting back pretty aggressively 10 years later.

Aslak Hellesøy (24:26):
But you can still do it. I mean, I’m working mostly in Typescript at the moment, and we are using a framework called Next.js, which is, I guess… It’s not quite Rails, but it’s a full-stack web framework. You can easily end up with the same kind of title-coupled code out there that you can only test with an acceptance test.

But what I’ve started doing is to adopt a more functional programming paradigm where I extract lots of small functions, and I unit test those functions in isolation. You can do the same thing with Ruby.

Aslak Hellesøy (25:01):
You have a lot of business logic in your model, well, just extracted to… What is it called? A PORO, Plain Old Ruby Object. And then you can test that in a unit test without Rails. But you have to be aware of these problems.

I think a lot of people don’t think about abstractions and decoupling and haven’t really experienced the pain. By the time you experience the pain, it feels like so much effort to try to undo that. But you can do these things incrementally.

My rule of thumb is that when I’m working on the legacy system, I only improve the things that I’m working on. So I’ve got to implement a new feature here and it’s a big hairball, fine, okay, I’ll spend a bit of time extracting that so that I can TDD on that little quarter of the code, and then things are a little bit better, and I’m a little bit happier.

My rule of thumb is that when I’m working on the legacy system, I only improve the things that I’m working on

-Aslak Hellesøy, Cucumber Founder

How to decide what to improve in the system Darko Fabijan (25:46):
Working as a consultant, and then as you are entering a big and old project, it really forces you. You have a lot of limitations that you must have a focus point, “What are you trying to improve?”

I remember that helping me a lot because I cannot go ballistic on various parts of the system, and, “Oh, I’m going to improve everything,” knowing you are not.

You’re tasked with improving this one component. And that kind of discipline, which is dictated by external factors, was a pretty fruitful thing for me.

Aslak Hellesøy (26:17):
Your opinion of what is good and bad is probably similar to my opinion of what’s good and bad, although we probably have some differences, and everybody has a different opinion on what’s good and bad.

Another practice that I think is really important in order to have a nicely testable code base with fast and reliable tests and so on is to have shared code ownership. If I go in and change a bunch of stuff, that’s not really going to fly, unless everybody else in the team agrees with my approach. It has to be the team’s approach.

Aslak Hellesøy (26:51):
So in my experience, if you want to do BDD and TDD, you kind of have to mix it with practices like, at least, occasional pair programming or mob programming to build that shared ownership of the code, because it means introducing obstructions that you wouldn’t otherwise have introduced like decoupling.

Everybody needs to understand and see the benefits, otherwise, they’re just going to throw their arms up in the air and go, “Why did you do all of this stuff? We can do it much simpler.” Yes, but it’s so that we can have a better test suite and work faster.

Darko Fabijan (27:23):
Yeah. It’s about investment. It’ll pay in the long term. What you’ll pay off in the short term versus long term are usually somewhat competing in a post.

Aslak Hellesøy (27:33):
Yeah.

The future of Cucumber Darko Fabijan (27:34):
Well, great. Many war stories that we have here with TDD and BDD, and embracing them. For Cucumber, you mentioned that there is a team working on it. What are some of the things that are currently ongoing with Cucumber for the future?

Aslak Hellesøy (27:53):
Oh. Yeah, loads of things. So since we were acquired by SmartBear, there’s a small team that’s paid to maintain Cucumber, or Cucumber Open as we’ve now officially named it. So that’s Matt Wynne, or Aurélien Reeves, and myself.

But there is also a team of maybe six, seven, eight regular contributors outside of the company. We meet up every Thursday at 3:00 UK time. We have about an hour meeting where we just sync up, it’s like a long standup meeting, where we talk about what we’ve done and what we’re going to do and various stuff.

There are now many implementations of Cucumber. Originally, it was Ruby, but there’s Java implementation. There is a Javascript/TypeScript implementation. There’s a Go implementation. We’re maintaining all of those.

There’s also the C# and .NET implementation, which is maintained by Tricentis, but we collaborate closely with them.

Aslak Hellesøy (28:54):
So we meet up regularly, and one of the things that have been the main focus over the past few years is to consolidate the implementation so that they have the same feature set, follow the same design, making it easier to maintain everything because they’re more similar, because in the beginning they evolved and diverged quite a bit.

Gherkin parser and Cucumber expressions So now we’re at a place where things are pretty compatible. Cucumber is built up of several smaller libraries. There’s a Gherkin parser. We share the same test suite for all of the implementations in Java, Ruby, .NET, and so on. We have something called Cucumber Expressions, which is an alternative to regular expressions with the possibility to extend them. We have tag expressions for filtering out scenarios.

It’s a little building expression language and a few other smaller libraries. So the approach we’re taking is, we store them in the same repository. They share an acceptance test suite, and we try to implement them more or less consistently using the same method, names, and class names, and so on. That’s worked out really, really well.

Aslak Hellesøy (29:59):
One of the things that Matt has been focusing on in the past few months is to grow the contributor community because Cucumber is used by, I don’t know, hundreds of thousands of people, maybe over 1 million. And as you can imagine with an open-source project that’s that popular, there’s quite a lot of work to do.

Even though we are a full-time team of two, three people plus regular contributors, we’re about 10 people, that’s not enough. So we need to grow the community. Math and Aurélien, they’ve been working on trying to recruit new contributors, helping people who have never contributed to open source before, and showing them how to get involved, is actually streaming on Twitch every Friday.

That’s been working really well. We’ve also improved the developer documentation so it’s easier to get onboarded, figure out where to start, and so on. So I think that work is really important.

Code completion in Cucumber Aslak Hellesøy (30:52):
One big project that we have now is to build better support for IDEs and editors. When you’re working with feature files, you want to have the same excellent support that you get when you’re working with Java, or Ruby, or Go, or whatever, which means really good syntax highlighting. Code completion is very important because you might have a library of hundreds of step definitions.

So we’re building something called the language server, which is the technology pioneered by Microsoft and Visual Studio code. It basically lets you write an editor plugin that can be used in many different editors. So you don’t have to write a plugin for every single editor, you just write one. And then that will work with all editors that support the LSP, the language server protocol.

Aslak Hellesøy (31:36):
That’s something that we’ve been working on in the past year, and hopefully, that will be released in the next few months. We hope that’s going to make it just more pleasant for people to work with Cucumber.

We’ve also got an online reporting service where you can publish reports straight from Cucumber to a website. That’s one of the commercial products that we’re working on inside of SmartBear.

Darko Fabijan (31:58):
It’s great to hear that Twitch, and these things that were not around in the early 2000s when these products were started and generally the open-source, are great. I can relate to maybe young people who are now embarking on a journey, want to contribute to open source.

If I would have a chance 10, 15 years earlier to watch someone on Twitch how they do things… I mean, that’s a huge thing. It’s not easy to get access to someone’s knowledgeable workstation and look how they’re dealing with big and small challenges as they’re developing. Yeah, sounds great.

Cucumber Slack channel Aslak Hellesøy (32:36):
We’re a 13-year-old open-source project and we’ve gone through several different communication channels. We had a Google group mailing list that went on for quite a long time. And then the activity on that just became less and less because we’ve turned on a Slack channel.

So now we have a Slack channel with 10,000 or nearly 15,000 members that have completely eclipsed the Google mailing list. We’ve actually disabled the mailing list now. And then we see new things like video streaming becoming more and more important. People want instant gratification now. They don’t have the attention span to read a mailing list or write an email. It has to be instant gratification in video or Twitter messages.

Darko Fabijan (33:21):There is acomponent of that. There is, I think, also a component just being with other people, in some format, which with Slack, I don’t know the instant nature of it scares me in terms of connecting with open source project. But I know that many, many communities do use Slack for their communication. I guess there are some challenges.

Aslak Hellesøy (33:41):
The main challenge with Slack is that it’s not easily searchable and it also doesn’t keep history forever, at least not on the plan we’re on. It’s not a great collective memory of what has been said. Mailing lists are better for that, but they’re dated.

So I’m not 100% happy with Slack. It works pretty well, but sometimes, I want to go back to this old conversation, I can’t find it, or something important is said and it’s just scrolled past and you miss it. So it’s more like face-to-face communication.

Darko Fabijan (34:14):
Exactly. Anyone listening wants to join, here is an open-source project which is interesting, has been around for more than a decade, and will be around for decades to come.

Aslak, thank you so much for joining and sharing the history and the future of Cucumber and all about BDD that we talked about also. Good luck with the project.

Aslak Hellesøy (34:36):
Thank you. It was a pleasure, Darko. Take care.

The post Cucumber Founder Aslak Hellesøy on TDD and BDD appeared first on Semaphore.

View Details

Do you want to build your own CI/CD platform behind the security of your company’s firewall while still enjoying the simplicity of Semaphore?

Meet self-hosted agents. Now, you can host your own agents and customize the environment when you run your jobs. With self-hosted agents, you have full control over hardware, operating system, and software tools that your pipelines run on.

Here’s how self-hosted agents complement the existing Semaphore environment:

How do self-hosted agents work? When creating your agent type, you get a registration token. There are three types of tokens available:

  • Agent type registration token to start up the agent
  • Agent access token to coordinate the agent state with Semaphore
  • Job logs stream token to send logs back to Semaphore

Check out the docs to learn more about how to set up self-hosted agents.

Depending on the environment where you set your self-hosted agents, you can then create custom configurations when you run your jobs.

For instance, AWS allows you to scale up when running larger jobs and scale down during less busy periods of the day. You can configure the minimum and the maximum number of agents to run a job.

This way, the system can adapt dynamically to your needs. Read more about the Agent AWS stack.

How to begin with Semaphore’s agents, step by step Step 1. Your team installs the agents on your machines. Semaphore supports all platforms, including Linux, macOS, AWS, Google Cloud, Kubernetes, and Windows (coming soon).

Step 2. The agent attempts to register with the Semaphore API.

Step 3. Status and output are reported back to Semaphore and displayed in the Semaphore UI. Your team has a full overview of the pipeline.

Why choose self-hosted agents? Semaphore’s agents help you to get full control over your CI/CD. Here are some of the main reasons why you’d want to choose Semaphore’s self-hosted agents for your organization:

  • Operation halting. You have full control over operations. In case your team notices anything suspicious, Semaphore has a master switch that halts all operations, instantly and completely.
  • Encrypted by default. All communications are fully encrypted, so that your team can benefit from the enterprise-level security that Semaphore provides.
  • Open source. Semaphore’s Self-Hosted Agents are built in the open. Your team can review and approve every decision
  • Only necessary access. Semaphore follows the principle of least privilege. Every token that is used for communication is restricted to the minimal sets of actions it needs to do its job.
  • Unidirectional communication. Communication always goes from agents to Semaphore, and never the other way around.

How Semaphore’s agents can help your team With Semaphore’s agents, you can run your jobs in an environment that is controlled by your team.

Self-hosted agents allow your team to have full control over hardware, operating system versions, and the available software. You can run the agents anywhere you want: physical or virtual machines, containers, or in the cloud.

With the help of agents, you can speed up your CI/CD dramatically, removing all disk limitations and caching what’s necessary.

Sign up for a private beta Self-hosted agents are currently in private beta. If you’d like to run Semaphore agents on your infrastructure, join the waitlist.

Here you can check out the Semaphore agent AWS stack.

Check out the docs to learn more about setting up Semaphore self-hosted agents.

The post Introducing Self-Hosted Agents appeared first on Semaphore.

View Details

Testing is an essential thing. You cannot be sure that your software project works if it lacks automated tests. There are dozens of awesome CI/CD tools that take the testing perspective to a new level (e.g. Semaphore).

Testing today is not just green and red icons in the IDE. Meaningful charts, descriptive logs, coverage percentage checks, and even individual running results are all available to the modern developer. Today, testing is an integral process of efficient software development.

When developers talk about testing, they usually mean unit-testing. But what about integration testing? Some people say that they are “too complicated”, and try to avoid them. This is a bad practice.

Tools, libraries, and CI/CD environments can make integration testing as straightforward as unit testing! How is this possible, you ask? Let’s find out.

Table of contents:

  1. Unit vs Integration Testing
    1. What is Unit Testing
    2. What is Integration Testing
  2. The Benefits of Integration Testing
  3. Integration Testing Pitfalls
  4. Known Patterns
    1. Manual Environment Configuration
    2. Vagrant
    3. Docker-compose
  5. Embrace Testcontainers
  6. Conclusion

Unit vs Integration Tests What is Unit Testing The purpose of unit testing is to validate individual component behaviour. By the way, there is a small amount of ambiguity regarding the definition of the word “unit”. Is it a class? A function? Or maybe a whole package?

It depends on your preferences. First, let’s define the Test Driven Development paradigm. TDD is a software engineering approach for writing tests before the business code. You can read more about TDD here. There are two schools of TDD. The Detroit School (or Classicist) and the London School (or Mockist).

The Detroit School promotes Inside-Out design as a rule. So, when we’re starting to work on a project, the domain model comes first. The API level is the last thing to be developed. The diagram below shows the steps of Detroit TDD.

Unit tests drive the architecture flow. You can compare it to an onion where each unit test covers an application with a new layer. Acceptance testing is the last step to verify that the whole software product is functioning as intended.

There is another important detail about the Detroit School of TDD. Unit tests can interact with many components (i.e. classes). When you realize that you need a new class, you instantiate it in your test suite. In the case of Detroit, “unit” describes the tests’ isolation but not the fact you have to verify one class at a time.

London School of TDD turns this paradigm upside down (i.e. outside-in). It declares that application development should start with an API. Whilst the domain model gets developed at the last moment. Take a look at the diagram below.

In the case of the London School, acceptance testing leads the development. Unit tests are completely isolated from each other. A single unit test only has to verify one component at a time, and any class’s dependencies should be mocked.

There are two opposite opinions regarding Unit testing principles. How do we define it then? I would state the definition in this way.

A unit test is a kind of automated test that has the following features:

  1. Unit test behaviour does not depend on any state outside the running application.
  2. Unit tests can run in parallel without affecting each other.

The unit testing approach is visualized in the diagram below.

We test the components themselves but not their interaction with the external system. To test these interactions, we need integration tests.

What is Integration Testing An integration test is a kind of automated test that has the following features:

  1. An integration test verifies components’ interaction with the external system (database, message queue, etc.).
  2. Integration tests may affect each other when run in parallel.

The goal of integration testing is to validate an application’s interaction with the external dependencies. Not a stub or a mock but the actual instance.

If you take a look at the integration testing approach diagram below, you can notice slight, but telling, differences from the unit testing approach.

Moreover, integration tests do not need to launch the entire application at once. The diagram distinguishes a database, a message queue, and mail service bindings as three separate integration tests. End-to-End (E2E) tests have the function of verifying the whole system’s correctness.

You can treat E2E tests as a superset for integration tests.

The Benefits of Integration Tests The same developers who decry integration tests as overcomplicated and unnecessary might be asking: why are they so important? Are you sure that we can’t just get rid of them and focus on unit tests?

While it’s true that integration testing brings some obstacles (we’ll discuss them later). The fact of the matter is that sometimes unit tests are not enough.

Suppose we’re developing an online bookshop. When a user opens a book card, it should also include its average rating. We also want to track every request to view a book’s information, so analysts can have more data to base business decisions on.

Here is a possible Java implementation with the Spring Boot framework:

``` public interface BookRepository extends JpaRepository {

@Query(""" SELECT b.id as id, b.name as name, AVG(r.value) as avgRating FROM Book b LEFT JOIN b.reviews r WHERE b.id = :id;""") Optional findBookWithAverageRating(@Param("id") long bookId);

}

@Service public class BookService {

private final BookRepository bookRepository; private final AuditService auditService;

public BookCard getBookById(long bookId) { final var book = bookRepository.findBookWithAverageRating(bookId) .orElseThrow(); auditService.bookRequested(book); return book; }

} ```

We could write a unit test for the BookService class here, but is it enough to be sure that the code is correct? The answer is no. Because the code is incorrect. There is a little detail that is easy to miss. Have a look at this query line:

WHERE b.id = :id;

This semicolon ; will cause an exception in runtime. So, even if you have 100% code coverage only in unit tests, you can still miss things like this.

Perhaps this example is not convincing enough. Spring developers (especially Spring Data ones) tend to notice such details. Let’s discuss something more complicated.

Assuming we need to retrieve a user by ID with roles, let’s write a possible query:

``` @Transactional(readOnly = true) public class UserRepository {

@PersistenceContext private EntityManager em;

public Optional findByIdWithRoles(Long userId) { List tuples = em.createQuery(""" SELECT u.id, u.name, r.name FROM User u JOIN u.userRoles ur JOIN ur.role r WHERE u.id = :id""", Tuple.class) .setParameter("id", userId) .getResultList(); // transform to dto return userView; }

} ```

The query does not produce runtime exceptions, but its behaviour is not always correct. You see, we put JOIN (alias for INNER JOIN) instead of LEFT JOIN. This means that we wouldn’t find users that have no roles.

You could test the aforementioned issues manually. Just launch the application locally and send HTTP-requests with Postman, right? Well, I would consider this approach to be incorrect.

The purpose of testing is to automate the validation of business features in order to increase the efficiency of the delivery pipeline. If you know that your code has been fully tested before merging it to the main branch, it’s much easier to deploy new versions. But, if the product is only partly verified, there is a greater chance that such a change might derail production entirely.

There is even a special term for this problem: Fear-Driven Development. Have you ever been afraid to refactor your code? Have you ever slept poorly because you merged something that was not truly validated the day before? Don’t blame yourself, because you’re not alone. Lack of integration tests causes this phenomenon.

My point is that unit tests are perfect to verify business logic because they are decoupled from implementation details. In reality, however, software applications interact with external systems, and we have to check those interactions to test our product. Integration testing is the perfect approach to solve the problem.

Integration Testing Pitfalls We’ve talked at length about the importance of integration testing, but not about the problems associated with its implementation.

Implementing integration testing can be tricky from the get-go. How do we start? The initial step is environment installation, and it’s harder than it seems.

  1. The application may interact with lots of external dependencies (e.g. PostgreSQL, Kafka, MongoDB, and so on).
  2. We also have to make the setup reproducible on any machine, because most projects are being created by groups of developers.
  3. Maintenance issues remain. For example, if one day our product starts to depend on another external service, we have to update the environment accordingly.
  4. Finally, this whole domain has to run during the Continuous Integration process.

Known Patterns Throughout the history of software development, programmers have proposed several patterns to approach integration testing issues.

Manual Environment Configuration The idea is simple. If your application depends on X, install X on your computer. Every time you run integration tests, enter properties according to the configuration file.

Seems like a natural approach, right? This is what we do to launch the application, after all. So, the testing part should not be different, right? Sort of. There are a number of issues:

  1. The developers are completely responsible for maintaining the environment. If somebody has upgraded the database version, everyone in the team should repeat it. Otherwise, the testing perspective won’t be reliable.
  2. This adds additional difficulties to configuration. Open port management is the least of the problems.
  3. This technique is inapplicable to CI.

You may disagree with the last point. For example, if the application needs PostgreSQL, we can run an instance on the remote server. Then anybody can connect to it during a pull request build.

Suppose there are two builds in two different projects running simultaneously. They may require completely different table structures. If both of them connect to the same database, neither will succeed.

Also, you could try to create and destroy databases dynamically on each CI build, but it would be rather laborious. There are better approaches that we’ll examine later.

Vagrant Vagrant automates the manual approach. You need to declare your dependencies in the Vagrantfile and execute the vagrant up command. Then the instrument runs a bunch of virtual machines. Each of them represents a particular external dependency.

What are the benefits?

  1. The tool follows the Infrastructure as code (IaC) methodology. The whole system configuration is a simple text file. The developers can change it via pull request.
  2. You don’t have to worry about versioning or any infrastructure substitutes. Vagrant picks up all changes.
  3. Manual managing is not required. Forget about open port issues or complex configuration obstacles.

Unfortunately, there are unresolvable points:

  1. You have to own a powerful machine to use Vagrant, because virtual machines require way more resources than solid service launching.
  2. The initialization will not be quick enough.
  3. CI integration is tricky (and sometimes is impossible). Technically Vagrant is not a testing tool, rather it is a development tool. Its main purpose is to prepare the environment for the local development process. Even though you can use it for testing, it does not solve major problems.

Docker-Compose Docker made a revolution in software development. To tell the truth, it didn’t invent anything new. Docker uses Linux namespaces and CGroups that have been around for a long time. Besides, the solutions already existed (e.g. LXC Containers), but Docker made container usage transparent and user-friendly. The simplest case requires a single docker run command.

Docker-Compose is the next evolutionary step. It allows several containers to be run on-demand. One should create a docker-compose.yml file to define all required services in a declarative way.

Sounds like a brilliant opportunity to approach the integration testing problem. Let’s have a look at what Docker-Compose offers:

  1. Docker-Compose follows IaC methodology like Vagrant.
  2. Since Docker is a cross-platform tool, the environment is reproducible anywhere.
  3. Zero-configuration. You only need Docker installed, and then to execute the docker-compose up command.

So, is Docker-Compose the key? Well, almost. The CI integration is possible but rather tricky.

The problem lies within the nature of Docker-Compose itself. You see, Docker containers are inaccessible from the host operating system by default. You have to specify the ports that should accept packets and transfer them to the container. It is no surprise that those ports should not be in use by any other program. Since docker-compose.yml is a regular text file, it’s necessary to define ports statically. For example, here is a possible way to run a MySQL database:

version: '3.3' services: db: image: mysql:5.7 environment: MYSQL_DATABASE: 'db' MYSQL_USER: 'user' MYSQL_PASSWORD: 'password' MYSQL_ROOT_PASSWORD: 'password' ports: - '5555:3306' expose: - '3306'

The port 5555 accepts the connection. OK, looks good so far. How do we know that the port is open on the CI node? Well, there is a hack to overcome this restriction.

  1. Put a placeholder (e.g. $MYSQL_PORT) instead of the actual port number.
  2. Run a special script that will check all ports and choose the first accessible one. Then the process replaces the placeholder with the found port.
  3. Run containers.
  4. Run tests.
  5. Stop containers.

Here is the schema showing the described algorithm.

Is that it? The silver bullet? Not quite.

1. Containers may keep running on build crashes.

Assume that something went wrong and the OS killed the process that was running the build. What will happen to containers? Nothing. They will keep running as usual. If such a scenario happened several times, that would lead to unnecessary resource consumption.

But, this issue is not the end of the world. We can create a job that runs on a schedule and terminates idle containers. It is, however, not the only problem.

2. The build itself may run inside a Docker-container.

That’s a common approach for many CI providers. It helps to run different builds in isolation. But this means that one loses the opportunity to run Docker containers.

It’s possible to run Docker containers inside another Docker container. The requirements are:

  • The outer container has to start in privileged mode (–privileged=true)
  • You should install docker inside the running container.

The thing about this approach is that you usually can’t control the properties of the container that runs your build. In this case, Docker-Compose is not a working solution.

Embrace Testcontainers Testcontainers is a Java library that creates the required dependencies as Docker containers when the tests start running, and eventually destroys them when the tests are complete.

You might point out that this solution is not so different from Docker-Compose. We still have to deal with idle containers on build crash and the possibility of the build running itself inside Docker container. I can say that Testcontainers overcomes these obstacles. We’ll see how it’s done later.

Here is a simple Java test with JUnit5 to integrate Testcontainers. I took the code example from the library documentation.

``` @Testcontainers class MixedLifecycleTests {

// will be shared between test methods @Container private static final MySQLContainer MY_SQL_CONTAINER = new MySQLContainer();

// will be started before and stopped after each test method @Container private PostgreSQLContainer postgresqlContainer = new PostgreSQLContainer() .withDatabaseName("foo") .withUsername("foo") .withPassword("secret");

@Test void test() { assertTrue(MY_SQL_CONTAINER.isRunning()); assertTrue(postgresqlContainer.isRunning()); }

} ```

The key difference between the Docker-Compose approach and Testcontainers is dynamic configuration. The containers are described as plain Java code. This gives much more flexibility in configuring the environment.

There are also no explicit port mappings. How can the application connect to the instance, you ask? Testcontainers does the job behind the scenes. It scans the available ports and chooses an open one.

What are the benefits of using Testcontainers?

  1. Simple configuration. You can tune the containers the way you want using the same language as the application’s code.
  2. The environment is easy to reproduce. Even if one cannot install Docker on the machine, it’s not a big deal, because you can configure the library to connect to the Docker service on the remote host.
  3. Dozens of ready-to-go solutions are packed into Docker containers for your use. If you don’t find the one you need, you can always apply the Generic Container.
  4. Though Java is a primary language for Testcontainers, there are plenty of other options–for example, Rust, Python, Go, Scala or NodeJS.

That all sounds promising, but what about the potential issues? We’ve already shown that Docker-Compose might be challenging to integrate into the CI pipeline. Does Testcontainers share the same problems?

  1. Containers may keep running on build crashes.

Testcontainers did have such an issue. Since Ryuk, however, container implementation is not relevant anymore. The idea is simple. Apart from the obligatory project dependencies, Testcontainers starts Ryuk. Its job is to track the health status of other containers by sending heartbeat requests. When a container stops answering, Ryuk deletes it with the corresponding image, network, and volumes.

  1. The build itself may run inside a Docker-container.

The library can detect that the application itself is inside a Docker container. To overcome this obstacle you should apply the Docker wormhole pattern.

Here is the code example:

docker run -it --rm \ -v $PWD:$PWD \ -w $PWD \ -v /var/run/docker.sock:/var/run/docker.sock \ maven:3 \ mvn test

When you run a build, you should map the volume and the working directory as the current one, and also mount the docker.sock file. Testcontainers will do the rest.

You probably won’t have to perform these configurations by yourself. Because most CI/CD tools on the market, e.g. Semaphore, support this pattern by default.

Conclusion In the end, I can say that integration testing is tough indeed (but worth it!). On the other hand, it has never been easier than today. The thriving of modern technologies (e.g. Docker, Testcontainers, CI/CD instruments) has made it obvious and straightforward. This is certainly the case with Semaphore, which supports running Docker containers as well as the Testcontainers library usage out-of-box. Meaning that you don’t have to deal with any complex configurations–Semaphore does it for you!

I’ve heard it said that high code coverage does not prove the code’s quality. Well, I can say for sure that lack of integration tests is the marker of a buggy product.

That’s all for now. If you have any questions or suggestions, please, leave your comments down below.

Thanks for reading!

The post Getting Integration Testing Right appeared first on Semaphore.

View Details

Professional software development is a feedback-based process — each new iteration is informed by past results. Feedback is powered to a considerable degree by tests.

Slow tests affect development. Engineering teams lose momentum and become frustrated because they can’t meet their goals. A slow test suite puts the brakes on CI/CD, making release and deployment more difficult. This often means that organizations can’t ship out products on time, and risk losing their competitive edge.

Choosing a scalable cloud platform like Semaphore is a great start. Semaphore offers some features that are helpful in dealing with slow tests, which we will discuss later in the article.

What’s wrong with waiting for tests? Tolerating a slow test suite is like making the minimum credit card payment when you could pay off your balance: by not dealing with it now you have a bit more cash in the short term, but will have to pay much more down the road. It doesn’t make any sense, but people do it because the costs are not immediately obvious. When faced with slow tests, developers typically respond in one of three ways:

  • Do something else and pay the cognitive cost for the context switch.
  • Wait for results and lose focus on the problem at hand.
  • Trudge on blindly without feedback.

Whatever happens, development speed falters due to the lack of timely feedback.

Fortunately, we have a sound plan that makes identifying and fixing slow tests much easier.

A framework for making slow tests fast Dealing with slow tests requires both a concerted effort and a sound plan:

  1. Identify: which tests are bad performers.
  2. Prioritize: pick a batch of the slowest tests. See if there are some outliers that could be easy to fix.
  3. Profile: zoom in and capture metrics to find out what your tests are doing behind the scenes.
  4. Optimize: make the tests snappy.
  5. Repeat: go back to Step 1 and repeat the process until you test suite is in top shape and your team is 🥇

Let’s be clear. This is not a one-off endeavor. It is part of the lifecycle of the project. Over time, tests slow down as the codebase grows and more tests are added. Therefore, you’ll need to repeat the whole process at least once per quarter to be in good shape.

Step 1 — Identify high-value candidates It can be hard to find the slowest tests when you have an extended CI/CD pipeline. Luckily, Semaphore supports Test Reports, which provide an effective and consistent view of your test suite in a CI/CD workflow.

There’s a little bit of setup required: you need to configure the test’s output to the JUnit format, as well as add a few commands. The result is, however, well worth the effort. In the detailed dashboard, you can spot problems, filter skipped tests, or order them by duration.

Test Reports dashboard Once you have a list of slow candidates to work on, you’re ready for the next step.

Step 2 — Maximize optimization effort vs benefit Two factors come into play for deciding where to start: how much faster you can make a test and how long you need to optimize it. We’re going to grab the low-hanging fruit first.

In other words, we want to start working on tests that maximize:

The trouble is that the only certainty we have at this point is how long the test takes. Everything else that we have is an estimation. Consider starting with a few easy-to-fix tests or deleting ones that do not add value, even if there are slower candidates in your suite. Once you have a good grasp of the process, you can go after slower tests that require more substantial effort to optimize.

The testing pyramid can guide us here. The width of each level reflects the suggested ratio of tests for each type relative to the whole suite.

The pyramid tells us that a good test suite should have many unit tests, some integration tests, and a few end-to-end or acceptance tests. In contrast, slow suites tend to be more top-level heavy, i.e. the opposite of what they should look like.

The way forward lies in cutting the fat at the top, either by deleting some tests or moving them downwards.

Maybe an example can help at this point. Imagine that we want to write an acceptance test for an online music service:

Feature: Control playback ​ Scenario: play a song Given there is no song playing When user presses the play button Then the song should start playing ​ Scenario: pause a song Given a song is playing When user presses the play button Then the song should be paused

It’s a valuable test that checks a business-critical feature. You may be able to squeeze some extra seconds of runtime but you can’t ever delete it.

At the other extreme, we have this:

Feature: Search for music ​ Scenario: search song cannot have an emoji symbol Given the search box is selected When user types an emoji Then the user should be notified emojis are not supported

Here we have to ask ourselves why we are spinning up the entire application just to test a search box. This is a perfect example of “low-hanging fruit”. We can quickly improve our entire suite by rewriting this test as a unit test and moving it down the pyramid, as shown below:

@Test public void testSearchBoxShouldNotAcceptEmojis() { SongSearch search = New SongSearch(); ​ Exception exception = assertThrows(RuntimeException.class, () -> { SongSearch("😉"); }); ​ String actualMessage = exception.getMessage(); String expectedMessage = "Sorry. Emojis are not supported in song names"; assertTrue(actualMessage.contains(expectedMessage)); };

Step 3 — Debug and profile Semaphore’s Test Reports will point you in the right direction for this stage. As you zoom in on what’s going on, you’ll need more information about what the tests are doing. A few well-placed puts, inspect, or console.log can be enough to discover the cause of the slowness.

Some problems, however, resist optimization and need heavier tools. For such cases, you’ll need to use profilers to access metrics deep down in your code.

Profilers and debuggers come in many flavors and colors. You can be sure that your language has more than one. A profiler records how much time each instruction takes, letting you locate “hot spots” in your tests. Sometimes the results can be visually striking.

A flamegraph shows relations between statements and their relative impact Even the most general profiler tool will show you each statement’s accumulated time. These are called statistical profilers and give you a panoramic view of what the test is doing. An example of such a profiler is rbspy:

Other profilers, such as stackprof, trace everything that’s happening by line. These types of profilers usually need some instrumentation to be configured, as shown below:

StackProf.run(mode: :cpu, out: 'tmp/stackprof-cpu-myapp.dump', raw: true) do # code you want to profile here end

Steps 4 — Optimize Now we’ve gotten down to it–how to actually optimize your tests. The method of optimization depends on what’s causing the slow performance. Experience shows that slow tests fit into one or more of these nine antipatterns:

  1. Obsolete tests: tests not earning their keep in your test suite.
  2. Mammoth-sized tests: tests that are too long, too heavy, and don’t take advantage of parallelization.
  3. Tightly-coupled tests: tests that are interconnected and cannot be executed independently. They pull chains of dependencies that are difficult to refactor and debug.
  4. Sleepy tests: tests that are peppered with sleep statements instead of proper synchronization mechanisms.
  5. I/O-bound tests: tests that heavily depend on slow I/O such as disk, database, or network.
  6. Slow query tests: tests that make inefficient use of the database.
  7. UI zealous tests: tests that always test through the UI even if there’re better ways of achieving the same result, such as hitting an API instead.
  8. UI completionist tests: these test the last corner of the UI and attempt to cover every edge case.
  9. UI prepper tests: tests that set up every test via the UI instead of using faster out-of-band methods, making them slower and more brittle.

How to fix these problems it’s a tale for a future post. Subscribe so you don’t miss it!

Step 5 – Rinse and repeat Like a diet, the first few iterations of your optimization effort will bring the most visible results. Keep repeating the process until the total test time is shorter than the time it takes to stretch your legs. Fasts tests will keep developers happy and onboard.

For reference, proper continuous integration can happen only when it takes 10 minutes or less. So plan your tests to fit in that window.

Conclusion Upon seeing a slow test suite for the first time, you may feel overwhelmed and dispirited. It may seem like there is no way that you can fix so many things and still be productive! But by focusing first on a small number of outlier tests and letting Semaphore’s debugging features aid you, you can speed up your suite a great deal with a few hours’ work.

Tests should help development, not block it. Timely feedback and a fast CI pipeline are the safety net that lets the team experiment, enhance, and release frequently. Every second shaved from a test is a compounded win. Make fast tests a priority today!

Read next:

  • What is Test-Driven Development?
  • Revving up Continuous Integration with Parallel Testing
  • Automated Testing: The Cornerstone of CI/CD

Thanks for reading!

The post 5-Step Strategy for Optimizing Slow Tests appeared first on Semaphore.

View Details

In this podcast episode, I welcome Emily Bache, Author, Technical Agile Coach with ProAgile. We talk about agile coaching, the Samman Method, peer reviews and various communication techniques and methods. Listen to our insightful conversation or read the edited transcript.

What we talked about:

  • The Coding Dojo Handbook
  • Starting a career as an agile coach
  • The Samman Method
  • How long does it take a team to embrace something new?
  • Communication techniques and methods
  • How to begin with ensemble working
  • How to improve soft skills and team work in an organization
  • How to deal with flaky tests and large test suites

Connect with Emily:

  • on Twitter
  • GitHub
  • Emily’s website and sammancoaching.org
  • Emily’s books on Leanpub

You can also get Semaphore Uncut on Apple Podcasts, Spotify, Google Podcasts, Stitcher, and more.

Like this episode? Be sure to leave a ⭐️⭐️⭐️⭐️⭐️ review on the podcast player of your choice and share it with your friends.

Edited Transcript Darko Fabijan:
Hello, and welcome to Semaphore Uncut, a podcast for developers about building great products. Today, I’m excited to welcome Emily Bache. Emily, thank you so much for joining us. Can you please introduce yourself?

Emily Bache:
I’m a software developer and technical coach based in Sweden, although I grew up in the UK, and I’m delighted to be invited to talk to you.

Introduction Darko Fabijan:
Great! Can you maybe give us some of the stepping stones through your career that led to you becoming a coach?

Emily Bache:
My career began in 1998 when I graduated with a degree in engineering and I got a job as a software developer doing Java. Then quite soon after that, in 2000, I moved to Sweden with my husband, and we’ve been in Sweden ever since.

And I landed on my feet because the first job I got in Sweden was a startup and it was Java again, and it was using extreme programming.

So, I really landed in this team of very good software developers. We were learning to write unit tests together and talking to the customer frequently in pair programming, and it was such a good experience.

I really got onto this whole agile and testing thing pretty early.

The Coding Dojo Handbook Emily Bache:
Since then, I had got a job doing some Python and worked some consultant for many years, and started training people in test-driven developments using the Coding Dojo. I wrote a book about that in 2011, The Coding Dojo Handbook. And then I spent some time doing some architecture and test strategy.

Emily Bache:
And then in 2018, I was invited to pair coach with Llewellyn Falco in the US. He showed me how he was not only teaching teams TDD but also refactoring, handing lazy code, etc.

I was so inspired by that I thought, “Right. Well, this seems to work.” So, I started doing technical coaching myself, and that turned into another book that was published about a year ago, “Technical Agile Coaching with the Samman Method“.

So, that’s what I do these days. I do technical coaching.

Darko Fabijan:
Great. And we are going to dive further deeper into that.

For some of the people who have been doing software development for a while, there are paths that they can take. They can move into management. They can stay in the software and do hands-on stuff.

Coaching is something that’s less frequent. Did it come natural to you over the years because you were helping people and enjoying that? How did you end up being a coach?

Starting a career as an agile coach Emily Bache:
At first, it was self-preservation. I was ending up on a team where I was the only one writing any tests and I was like, “This is not fun.”

The tests are breaking all the time and nobody cares so I’ve got to do something to persuade my colleagues to pay attention. So, it kind of grew from there that I found I actually quite liked holding little trainings and dojos. That’s where it started.

Emily Bache:
The thing is, I could have gone into coaching full-time but I think as I really liked writing code, I didn’t want to give that up. And I still don’t.

So, this thing with coaching and teaching is a way to have influence and be senior, and show technical leadership without losing touch with the code. It combines this people stuff with technical stuff in a really fun way, in my opinion.

And I think I have more influence as a coach than I would have as an individual contributor. I certainly meet more teams and interact with more people.

Darko Fabijan:
Yeah, yeah. And hearing you’re fit in both areas is super important. Now that you said that, because I did meet a lot of people doing trainings one way or another, and it can really show if this guy or girl haven’t touched the code in a while.

Emily Bache:
Yeah. You know, this industry moves so fast. You really have to be writing a lot of codes to keep up with all the latest frameworks and tools, and languages, and everything.

The Samman Method Darko Fabijan:
You’ve also published the book about the Samman Method. We don’t know anything about it. Could you please tell us more?

Emily Bache:

I just felt it needed a name for the way I’m working, just to make it easier to find out more information and find other people who are doing this kind of thing. The name came long after the coaching approach, which I’ve been doing for a few years.

Emily Bache:
As I said, Llewellyn Falco is a great inspiration for me. And there’s a whole community of coaches who are doing ensemble working or people call it mob programming as well with teams as a way of transferring knowledge and skills and writing better code, and having more fun at work.

That’s a major part of Samman method is doing ensemble working. That’s the whole team programming together on one task and delivering that code, trying to get everyone in the team to contribute to that piece of code so that we get the best of all of us into it.

The major part of the Samman method is doing ensemble working. That’s the whole team programming together on one task and delivering that code.

-Emily Bache, Technical Agile Coach, Author

Emily Bache:
And as a coach, you’re there to facilitate that, to make sure that the team is able to collaborate in that way. But you also need to contribute what you know about how to write tests and how to refactor safely so that the team learns those things as well.

So, working in the production code with a team in an ensemble is one part of the Samman Method.

Emily Bache:
And then the other part is, if you’re going to learn something like TDD or refactoring, there’s theory, skills, vocabulary, and exercises that you can do that will help to you to learn those things in a more effective way.

So, the other part of the Samman Method is these short teaching sessions, where we do exercises and we learn skills, and we practice.

Emily Bache:
Those are the two main parts of the Samman Method. As a coach, I come to a team and I facilitate ensemble working, and I lead learning hours. And I don’t do that full time, the team has to have time to do their other stuff, get tasks done up with me not being there.

Emily Bache:
As a coach, I can work with several teams concurrently or I can work part-time, and I can go to a lot of conferences and stuff, if that’s what I feel like doing.

As a coach, it’s a great lifestyle, in a way. And for the teams I’m coaching, it’s the most effective way I know so far for introducing this kind of technical practices in teams that aren’t using them.

How long does it take a team to embrace something new? Darko Fabijan:
As you were speaking about this, I was contrasting this with my experiences and how I learned it, how I helped other people to just get on that strain of TDD. I would say also writing tests in general, because that’s also where our industry tends to struggle.

Can you please talk more about the timeline that you are seeing with the teams but introducing anything new?

Emily Bache:
There’s a lot of variety. I mean, most of the teams I go into and I start working with, they already have some unit tests and often, other integration tests and continuous delivery pipelines.

Often, they’ve got basic infrastructure. Also, quite often, I’ll find that maybe there’s one person in the team who is a bit more experienced and actually does really know their stuff already, and can write good tests, and do refactoring, and stuff.

But at the same time, the rest of the team is much more junior or perhaps less experienced with these techniques, and it’s not always easy for even one experienced person in a team to spread that.

And of course, sometimes you come to a team and nobody knows anything about this, and they’ve been told to write tests but they’ve no idea how to write good tests.

Teaching collaboration techniques Emily Bache:
So, as a coach, I come into this kind of situations. If the knowledge is already in the team and somebody there is experienced, then as a coach, it’s just a question of teaching collaboration techniques, really, so that they can learn to spread that knowledge and learn from one another.

That’s the biggest thing I contribute some of the time – the collaboration techniques, which is great in itself. I mean, ensemble working is a fantastic technique even without the rest of the learning hours and stuff that I do.

Teaching how to design good tests Emily Bache:
But then of course, if there’s really a lack of knowledge about how to design good tests and what TDD actually is.

It’s not just writing unit tests, TDD is a design technique. It’s a way of developing code and test together.

Then there’s, perhaps, a lot more focus on the learning hours at first of what is TDD. And quite often, you come in and the codebase is built in such a way that it’s really hard to write good tests.

And then, at that point, it’s a lot more like, “Well, can we do safe refactoring here?” Even with the tests we’ve got, perhaps we’re better off focusing on the refactoring techniques before we try and learn too much about testing.

So, the approach I take does vary according to the situation the team is in, definitely.

Communication techniques and methods Darko Fabijan:
Clear. And in ensemble practice, you mentioned your communication techniques or methods. Can we perhaps dive a bit deeper there?

Emily Bache:
Yeah. So, I don’t know if you’ve ever tried to program with six developers in the same room with one computer. If you just try and do that with no guidance, it could get a little but chaotic.

Every software developer has opinion about what the code should be and different ideas about where we should start and what we should focus on.

And so, a lot of the ensemble working technique is just working out how should we talk to each other in a way that actually means that we’re collaborating and not fighting with each other.

A lot of the ensemble working technique is just working out how should we talk to each other in a way that actually means that we’re collaborating and not fighting with each other.

-Emily Bache, Technical Agile Coach, Author

So, when I’m teaching it to a team that hasn’t done it before, we’ll go through, “Well, there are some roles. If you have this role, then you should say this. Otherwise, you’re quiet.”

So, it’s like I’m quite strict to the start. And then when you’ve got the hang of it a bit, you can start to loosen up the roles a bit.

Ensemble working technique in practice Emily Bache:
So, the way I teach it is that the person with the keyboard, who’s controlling the computer, that’s the typist. They are typing in the code that the rest of the team has decided on, not the code that they have in their head that they’ve decided on. That’s important.

The person with the keyboard is not driving the direction. They’re not making the decisions. They are just typing in the code. So, that’s the first thing.

Emily Bache:
And then the other thing is that in order for the typist to know what to write, they need to get some clear instructions from the rest of the team. So, we have the spokesperson who is called the navigator, and they’re the person who is supposed to be primarily directing the whole ensemble and making the decisions.

And they, of course, need to get help and input from the rest of the team. But they should be the person initiating that so that everyone else is mostly quiet until they get asked, or unless they really, really have to say something.

And then of course, you rotate these roles so that you don’t get completely stuck in one person taking over the whole time.

How long is a typical session? Darko Fabijan:
And what’s the duration of the session?

Emily Bache:
I would definitely take a break every hour for at least a few minutes. But when I’m coaching a team, we normally do a two-hour session on the ensemble.

And I think if you’re a beginner, that’s a nice length, two hours with a short break in the middle. And then when you’ve got used to the technique a bit, maybe you can vary that.

Darko Fabijan:
And on the topic, “Okay, what we are going to work on?” Because they have much deeper insight into their codebase. I understood that you are working on their codebase so this is not something which is examples or fictional.

How to begin with ensemble working Emily Bache:
Yeah. I mean, if you’ve really never done ensemble working before, it can be useful to begin with toy codes, katas, exercises just because it makes it so much easier to go focus then on the communication patterns rather than the code.

Darko Fabijan:
But you do progress?

Emily Bache:
Yeah. As a coach, I’m hoping to get to the point quite soon that we can go into their codebase.

The thing is, with something like refactoring and TDD, you can do it on the example code relatively easily. After a few sessions, you can be competent.

But then you go to your production code and suddenly it’s like, “Wow, this is so much harder.” And that’s the point where you really need somebody with a bit of experience, like me, to come in and help just say, “Okay, this is not straightforward but trust me a bit. Let me navigate for a minute and help you get started with this test.”

I think we need to focus on refactoring and try and prompt them into doing in small steps, and committing often. But yeah, you need to get some assistance, I think, in a lot of production code situations from someone with experience.

I think we need to focus on refactoring and try and prompt them into doing in small steps, and committing often.

-Emily Bache, Technical Agile Coach, Author

Darko Fabijan:
Yeah. Clear. And then this would go on for once, twice per week and then for a couple of weeks or…

Emily Bache:
Yeah. When I started doing this, it was like we’ll do two weeks, 10 days and then we’ll have a break. And then come back, do another 10 days. But I found that it works better for me to spread it out more, 10 days over three weeks, 10 half days, in fact.

And then I’m working with one team now, where we’re just doing one session a week. I think frequency is up for negotiation. It’s partly how quickly the team wants to get input and how much they are managing themselves, I guess.

Darko Fabijan:
Yeah. Yeah. I’m asking primarily because there is that moment when you are learning something and you think you get it, and then, “Oh.” A month and a half later, “Ooh, now I get it.”

Emily Bache:
Right. So, the engagements I have with teams, I would like it to be more long-term. So that when you realize, “Ah, now I get it,” or “Actually, now I’m stuck,” that the coach is still somewhere available and you can still get some help.

Darko Fabijan:
Yeah, exactly. If I were receiving the training, I’d like to be able to call the coach a month later and ask for help.

Emily Bache:
Learning takes time. It takes practice and effort. And actually, it’s not just learning new skills, it’s the whole culture change that you’re trying to get.

Emily Bache:
When you’ve got an agile team that really understands TDD and refactoring, and working in small steps, and incremental design, and all this, that’s a team culture, as much as skill. It’s a way of working that is so embedded in the habits of that team.

When you’ve got an agile team that really understands TDD, working in small steps, incremental design and all this, that’s a team culture as much as skill.

-Emily Bache, Technical Agile Coach, Author

That’s just the way they work. And that kind of culture change doesn’t happen after two weeks with a coach or a two-day training course. It’s a much longer term kind of prospect so you need a bit of continuity and somebody to be pushing for that.

How to improve soft skills and teamwork in an organization Navigating through code reviews Darko Fabijan:
On those cultural patterns, I do remember, when GitHub introduced pull requests, the majority of the civilization got access to code reviews as a good and accessible tool to do them.

I remember back then some people were taking those reviews personally. It’s not a communication pattern, though. It also depends on how someone is saying things.

Source: 9GAG In terms of soft skill patterns, do you intervene and help people improve communication?

Emily Bache:
Of course. I mean, yeah, teamwork. Software is built by teams these days, and teams need to be able to communicate in order to collaborate.

I mean, this is a constant challenge for me as coming in as a coach as well, just getting accepted by the team as somebody who’s worth listening to. So, there’s all those soft skills and culture, and communication, it’s really important.

Emily Bache:
And I won’t just drop into a team and say, “Right now I’m coaching you.” I always try and get to know them first, find out about their situation and try and discuss with them the challenges they’re seeing, and where I might be able to contribute something.

Code reviews and the Samman method Emily Bache:
We started here talking about code reviews. I mean, on Samman working, here’s kind of a constant code review. It’s kind of intense.

Darko Fabijan:
Yeah. Real-time code review.

Emily Bache:
Yeah. You need good people skills and good relationships for it to work smoothly.

How code katas work Darko Fabijan:
Yeah. And I think that it’s widely known and, hopefully, also accepted. Just that element of doing dojos or kata, or doing something over and over, it is a small thing and then improving over time.

I was introduced to that two years after I finished university. And during my university years, it never crossed my mind to solve one problem, write a function class, whatever element, entity of software over and over and got better at it.

So, could you introduce that concept to listeners who are not aware of it yet, please?

Emily Bache:
Yeah. If you were given a university exercise, why would you do it more than once?

The thing with code katas, what you’re training is not your ability to solve a problem or to demonstrate that you can use a technique at all.

The techniques that you are learning from the code kata is the way of working. It’s the process of producing code and tests. That’s what you’re actually trying to achieve.

The techniques that you are learning from the code kata is the way of working.

-Emily Bache, Technical Agile Coach, Author

Emily Bache:
And when you’re doing exercise at university, you do like 10 exercises, all kind of similar, until you’ve shown that you understand the technique.

With a code kata, what you’re practicing is the flow of code from your brain through your hands, into the computer, and how you do that in small steps, and how you choose the next test to write, and how you transform the duplicated, messy design into something that’s clean and dry in the refactor step, and how you do that safely.

Emily Bache:
So, it’s really the process of programming. It’s not the end result that matters. So, that’s why you do it more than once, because that’s actually what you’re getting better at.

It’s not doing 10 different exercises, doing the same exercise 10 times, but the process that you use is perhaps slightly different each of those 10 times, and you learn, you get better.

It’s not doing 10 different exercises, doing the same exercise 10 times, but the process that you use is perhaps slightly different each of those 10 times, and you learn, you get better.

-Emily Bache, Technical Agile Coach, Author

Emily Bache:
When you do it on the code kata, the idea is you can do it in your production code, and it’s harder in the production code.

But if you can do it really smoothly and without thinking about it in a code kata, then you’ve got so much more brain capacity to spend on your actual production code and its quirks and difficulties, and understanding what the customer wants.

The importance of setting daily habits Darko Fabijan:
Yeah. And then over time it, I guess, evolves into pretty much a habit formation, and that’s the main idea.

And with your coaching, the first part is very much a team and communication effort.

The second part can be done to pair programming, right? Over and over again.

How does that look in practice? How would you advise doing that?

Emily Bache:
I do these one-hour learning hour sessions. I structure them as a lesson that’s thought through and is trying to be engaging. And I use the 4C model from Sharon Bowman as a way to structure this and to make it interactive, because people learn by doing and by talking much more than they learn by listening and watching.

We’ll work that in pairs, sometimes all together. That is your chance to practice and to see the technique.

If you can get that kind of daily bit of practice, you’ll soon see some results that make it all worth it for your company. You’ll be so much more effective when you’re working the production code.

How to deal with flaky tests and large test suites Darko Fabijan:

In the last couple of episodes, I was touching upon the health of the codebase and the structure of the testing pyramid. A lot of teams that I end up talking to are asking for help and advice, like, “What are they doing to attain their test suite?”

Usually, people end up having the majority of their tests to be brittle and complex that are running through the whole stack of the application whereas what they wanted is to test something simple.

So there are two major things – the duration of those tests and how flaky they are. We’d love to hear your approach to this.

Emily Bache:
Yeah, I recognize that problem. The test pyramid is set up so that what you really want are fast tests that are cheap to maintain. And what you get, often, is slow tests that are really expensive to maintain and brittle.

Why writing unit tests is not

Emily Bache:
And usually, people say the way to get fast, cheap tests is to write unit tests.

The thing with that is, of course, you need to design your code in such a way that it’s possible to have unit tests. There have to be units, and a lot of codebases aren’t built that way, unfortunately.

So, if you’re in that situation where you haven’t got units and your tests are all big and slow, and brittle, it’s very annoying to have someone come to you and say, “Right. Well, you need to write unit tests.” Because you’re like, “Yeah, but that’s really hard. I can’t do that.”

What is approval testing? Emily Bache:
There’s a technique that I’ve been using a lot lately, well, not just lately, approval testing. This is a technique that people I know, including Llewellyn Falco and my husband, Jeff, have been pioneering this way of designing tests.

And I’m really excited about it right now, because I think it presents a middle way between slow and brittle tests and unit tests, but it does involve architecting your system slightly differently so that you can extract this kind of big units or subsystems.

And there’s a lot more to be written about this, but I’m really excited.

Emily Bache:
I’ve just been designing a new training for O’Reilly about this technique. I’ve called it hands-on agile software testing, and it’s really about architecting in a way that enables this kind of testability with approval testing.

I think it’s going to help so many people. And it’s one of the things I introduce often when I’m working with a team who I can see, hasn’t got units and refactoring is difficult.

This is one of the techniques that’ll bring in approval testing.

How to connect with Emily Bache Darko Fabijan:
For those of us interested in hearing and learning more about this technique, about you, can you tell us what are some of the ways that we can find and engage?

Emily Bache:
Yeah. So, I’m on Twitter – Emily Bache. And from there, you can probably find the rest of my stuff on the internet.

I’m on GitHub, and I’ve got a website also, sammancoaching.org, which goes with my book, which is published on Leanpub.

And this stuff I was just talking about, approval testing, as I said, I’m doing a live online training with O’Reilly and also with TechTalk, and more in-depth training on the same topic in March. Follow me on Twitter, and hopefully, you’ll get this kind of links.

Darko Fabijan:
Yeah, that’s a good starting point. Yeah. Emily, it was super interesting to talk about all these topics. Thank you so much. And yeah, good luck.

Emily Bache:
Thank you. It’s been really fun talking to you.

The post Agile Coaching and the Samman Method with Emily Bache appeared first on Semaphore.

View Details

A test that intermittently fails for no apparent reason — or works in your local machine and fails with continuous integration — is called a flaky test. Flaky tests hinder development, slow down progress, hide design problems, and can cost a lot of money in the long run.

A survey published in October 2021 shows that not even huge corporations like Google or Microsoft are spared from flaky tests — about 41% and 26% of their tests, respectively, were discovered to be flaky. The same survey showed how flaky tests threaten the efficiency of CI: 47% of failed jobs that were manually restarted succeeded on the second run.

As you can see, flaky tests are pretty common in the software industry. But they only become a serious problem when we’re not proactive about fixing them.

The problem with flaky tests An essential property of an automated test is its determinism: if the code hasn’t changed, the results shouldn’t either. Flaky tests neutralize the benefits of CI/CD and shake the team’s trust in their test suite.

You may be thinking that if a test fails randomly, you can game the system by retrying it until it passes. If this sounds like a good plan to you, you’re not alone. Some test runners even do this for you automatically.

The game is rigged While re-running flaky tests is a popular “fix” — especially, when a deadline is approaching — is it a real solution? How confident are you in your test after having taken this road? How often must the test fail until you declare it a “real” failure? There aren’t any satisfying answers. The game is rigged. You can’t win with this strategy.

Step 1 – Commit to fixing the problem right away! The first appearance of a flaky test is the best moment to fix it. Maybe the test is new, or a recent commit changed its stability. This means that the related history is still fresh in the developers’ memory and that they can act quickly. Don’t procrastinate!

Nothing is more frustrating than trying to push a hotfix down the pipeline and seeing a flaky test standing in your way. Flaky tests slow down development by affecting both the test suite and the CI pipeline. Retrying tests may temporarily solve the issue but, by slowing down CI/CD, you’re wasting time and reducing your capacity to deliver software.

If you don’t have enough time to fix a test right away, you should document it, create a ticket, and start working on it as soon as possible. Keep in mind that reporting a problem does not equal fixing it. And by not fixing it, the technical debt in your project grows. Adopting a “fix it now” mentality is the first step to alleviating problems stemming from flaky tests.

Step 2 – Find the flaky tests in your suite Flaky tests are statistical by nature, so you’ll need to follow a test over a few days to understand its behavior. The more it runs, the more likely a pattern will emerge.

One efficient way to do this is to use a CI service such as Semaphore. By setting up a schedule on the main or failed branch every hour (or more often), you will have enough data to document the problem within a couple of days.

Scheduling CI executions Another benefit of scheduling is that builds execute at different times of the day. If you notice a pattern — for example, a test fails only between 3 and 5 am — you are one step closer to fixing the test.

Saving debugging information Save every scrap of information that can help you find the root cause of the flakiness. Event logs, memory maps, profiler outputs — the key to the problem could be anywhere. Set your logging level to debug, like I did below with log4j, and print the messages into a file:

```

log4j.properties

log4j.rootLogger = DEBUG, FILE
log4j.appender.FILE=org.apache.log4j.FileAppender
log4j.appender.FILE.File=${log}/log.out ```

Feel free to add debugging messages or any other instrumentation to help you make sense of the logs.

import com.foo.Bar; ​ import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.LogManager; ​ public class MyApp { ​ private static final Logger logger = LogManager.getLogger(MyApp.class); ​ public static void main(final String... args) { logger.debug("Entering main loop."); Bar bar = new Bar(); if (!bar.doIt()) { logger.error("Didn't do it."); } logger.debug("Exiting main loop."); } }

SSH debugging For quick diagnosis, you can run a job interactively with SSH debugging. Semaphore gives you the option to access all running jobs via SSH, restart your jobs in debug mode, or start on-demand virtual machines to explore CI/CD.

You can reproduce the conditions that caused the test to fail and try ways of fixing it. The changes will be lost when the session ends, so you’ll need to re-apply any modifications as a normal commit in your repository.

`` sem debug job 0265bd94-e2d1-4d5c-b89a-88f918dbf3a2 * Creating debug session for job '0265bd94-e2d1-4d5c-b89a-88f918dbf3a2' * Setting duration to 60 minutes * Waiting for debug session to boot up .............. * Waiting for ssh daemon to become ready ....... ​ Semaphore CI Debug Session. ​ - Checkout your code withcheckout- Run your CI commands withsource ~/commands.sh- Leave the session withexit` ​ Documentation: https://docs.semaphoreci.com/essentials/debugging-with-ssh-access/.

semaphore@semaphore-vm:~$

```

A picture says more than a thousand words The most challenging class of flaky errors to debug involves the UI. End-to-end and acceptance tests depend on graphical elements not represented in logs.

Configure your test framework to dump HTML or screenshots when a test fails. You’ll be happy to have something to look at when the error strikes. The following example shows how to save a rendered page as an image with Cucumber.

// Cucumber supports hooks that allow you to execute code after a scenario has failed. @After public void afterAcceptanceTests(Scenario scenario) { try { if (scenario.isFailed()) { final byte[] screen = ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES); scenario.embed(screen, "image/png"); } } finally { driver.quit(); } }

Here’s the same thing but with Ruby and another BDD framework called Capybara, which also allows you to take screenshots in tests.

after(:each) do |example| if example.exception # print DOM to file print page.html # save screenshot of browser page.save_screenshot('screenshot.png') end end

Configuring reports in the pipeline Every job in Semaphore runs in an isolated environment, which means that all generated files (including any logs) are lost when the job is done. Without logging information, it’s very difficult to find the root cause of flakiness. In order to preserve files, Semaphore provides two mechanisms: artifacts and test reports.

Artifacts are the easiest way to data from your pipelines:

  1. Save your files in a predetermined location.
  2. Add the following command in the CI job: artifact push job <my_file_or_dir>
  3. Download the files in the Artifacts tab of your job.

Artifacts saved in the pipeline Test reports require more setup, but they are much more powerful. This feature generates a categorized and sortable dashboard with all the tests in your pipeline. It works with any test framework as long as it outputs the results in the JUnit format. For more information on setting up test reports, check out the test docs.

Test Results Step 3 – Documenting flaky tests After driving the flaky tests out into the open:

  1. Document every flaky test in your ticketing system.
  2. As you acquire more information about the cause of a test’s flakiness, add it to the ticket.
  3. Feel free to fix tests right away if the reason for their flakiness is readily apparent.

Too many open tickets are a good indicator that some time needs to be set aside to improve the test suite’s quality. Also, a ticket is an excellent place to discuss ideas for fixes.

Step 4 – Determining the cause and fixing the test In some cases, the cause of failure is obvious. This means that you can fix the test and close the case quickly. The problem arises when it’s not immediately clear why a test is failing. In such cases, you will need to analyze all the garnered data.

Let’s look at common causes for flakiness and their solutions.

Environmental differences Differences between your local development machine and CI fall into this category. Variances in operating systems, libraries, environment variables, number of CPUs, or network speed can produce flaky tests.

While having 100% identical systems is impossible, being strict about library versions and consistent in the build process help to avoid flakiness. Even minor version changes in a library can introduce unexpected behavior or even bugs. Keeping environments as equal as possible during the entire CI process reduces the chance of creating flaky tests.

Containers are great for controlling what goes into the application environment and isolating the code from OS-level influence.

Non-deterministic code Code that relies on unpredictable inputs such as dates, random values, or remote services, produces non-deterministic tests.

Preventing non-determinism involves exerting a tight degree of control over your test environment. One thing you can do is to inject known data in place of otherwise uncertain inputs using fakes, stubs, and mocks. These devices allow you a great degree of control over otherwise random inputs in your tests.

In the following example we override now() with a fixed value, effectively removing the non-deterministic aspects from the test:

@Test public void methodThatUsesNow() { String fixedTime = "2022-01-01T12:00:00Z"; Clock clock = Clock.fixed(Instant.parse(fixedTime), ZoneId.of("UTC")); ​ // now holds a known datetime value Instant now = Instant.now(clock); ​ // the rest of the test... }

Asynchronous wait Flaky tests can happen when the test suite and the application run in separate processes. When a test performs an action, the application needs some time to complete the request. After that, it can check if the action yielded the expected result.

A simple solution for asynchrony is for the test to wait for a specified period before it checks if the action was successful:

click_button “Send” sleep 5 expect_email_to_be_sent

The problem here is that, from time to time, the application will need more than 5 seconds to complete a task. In such cases, the test will fail. Also, if the application typically needs around 2 seconds to complete the task, the same test would be wasting 3 seconds every time it executes.

There are two better solutions to this problem: polling and callbacks.

Polling is based on a series of repeated checks of whether an expectation has been satisfied.

click_button "Send" wait_for_email_to_be_sent

In this case, the wait_for_email_to_be_sent method checks if the expectation is valid. If it’s not, it sleeps for a short time (say, 0.1 seconds) and checks again. The test fails after a predefined number of unsuccessful attempts.

The callback solution allows the code to signal back to the test when it can start executing again. The advantage of this is that the test doesn’t wait longer than necessary.

Imagine that we have an async function that returns a value by reference:

function someAsyncFunction(myObject) { // function body ​ // return value by reference myObject.return_value = "some string"; }

How do we test such a function? We can’t simply call it and compare the resulting value, because the function may not have been completed yet by the time the assertion is executed.

// this introduces flakiness let testObject = {}; someAsyncFunction(testObject); assertEqual(testObject.return_value == "some string");

We could put a sleep or some kind of timer in place, but this kind of pattern itself could introduce flakiness. It is much better to refactor the async function to accept a callback, which is executed when the body of the function is complete:

// run a callback when the function is done function someAsyncFunction(myObject, callback) { ​ // function body ... ​ // execute callback when done callback(myObject); } ​ // move the test inside the callback function function callback(testobject) { assertEqual(testobject.return_value == "expected string"); };

Now we can chain the test to the async function, ensuring that the assertion runs after the function is done:

let testObject = {}; someAsyncFunction(testObject, callback);

Concurrency Concurrency can be responsible for flakiness due to deadlocks, race conditions, leaky implementations, or implementations with side effects. The problem stems from using shared resources.

Check out this test for a money transfer function:

function testAccountTransfer(fromAccount, toAccount) { lockFrom=fromAccount.lock() lockTo=toAccount.lock() ​ beforeBalanceFrom = getBalance(fromAccount) beforeBalanceTo = getBalance(toAccount) ​ transfer(fromAccount,toAccount,100) ​ assert(beforeBalanceFrom - getBalance(fromAccount) == 100) assert(getBalance(toAccount) - beforeBalanceTo == 100) ​ lockTo.release() lockFrom.release() }

If we were to run multiple instances of this test in parallel, we would risk creating a deadlock in which each function locks out the resources that the other needs, developing a state in which neither test ends.

// both tests running in parallel can cause a deadlock testAccountTransfer('John', 'Paula') testAccountTransfer('Paula', 'John')

This sort of failure can be prevented by replacing the shared resource (the account) with a mocked component.

Order dependency Order dependency problems are caused when tests are executed in a different order than planned. One way to solve this issue is to consistently conduct tests in the same order. This, however, this is a poor solution, as it means that we have accepted that tests are brittle and that their execution depends solely on a carefully built environment.

The root of the problem is that these tests depend on shared mutable data, and when it’s not mutated in a predefined order, the tests fail. This issue is resolved by breaking dependency on shared data. Every test should prepare the environment for its execution and clean it after it’s done.

Look at this Cypress test and think about what would happen if we reversed the test order of subscribing and unsubscribing.

describe('Newsletter test', () => { ​ it('Subscribes to newsletter', () => { cy.visit('https://example.com/newsletter'); ​ cy.get('.action-email').type('fake@email.com'); cy.get('.subscribe-button').click(); ​ cy.get('.message').should('have.value', 'Subscribed successfully'); }); ​ it('Unsubscribes from newsletter', () => { cy.visit('https://example.com/newsletter'); ​ cy.get('.action-email').type('fake@email.com'); cy.get('.unsubscribe-button').click(); ​ cy.get('.message').should('have.value', 'Unsubscribed successfully'); }); ​ });

In addition to issues with data in the database, problems can also occur with other shared data, e.g. files on a disk or global variables. In such cases, a custom solution should be developed to clean up the environment and prepare it before every test.

Improper assumptions One has to make assumptions when writing tests, e.g. expecting a dataset to already be loaded in the database, etc. Sometimes, however, reality surprises us with a day that has less than 24 hours.

The best we can do is to make tests completely self-contained, i.e. prepare the conditions and set up the scenario within the test. The next best thing is to check our assumptions before executing them. For instance, JUnit has an assumption utility class that aborts a test (but does not fail it), if the initial conditions are not suitable.

``` @Test void testOnDev() { System.setProperty("ENV", "DEV"); Assumptions.assumeTrue("DEV".equals(System.getProperty("ENV"))); }

```

Fixing the test After acquiring more data about a test failure, you should start fixing tests. Once you fix a group of tests, merge the branch back into the mainline to pass the benefits to your team.

If all else fails, the offending test should be deleted and re-written from scratch, preferably by someone who wasn’t involved in writing it the first time.

Conclusion Remember, it’s easier to write a test than to maintain it. Strategies for fixing a flaky test are highly dependent on the application, but the approach and ideas outlined in this article should be enough to get you started.

Is flakiness still plaguing in your codebase? Read these next:

  • 3 Steps to Eradicate Flaky CI Builds
  • Flaky Tests: Are You Sure You Want to Rerun Them?
  • Test-Driven Development (TDD): A Time-Tested Recipe for Quality Software

The post How to Find and Eliminate Flaky Tests appeared first on Semaphore.

View Details

If you use Bazel to build your monorepos, at some point you’d need a CI/CD solution like Semaphore that supports monorepos out of the box. So in this roundup, I’ll walk you through how you can setup and run your Bazel builds on Semaphore.

What Is Semaphore? First, let’s understand briefly what Semaphore is and why it’s useful.

Semaphore in a Nutshell Semaphore is a CI/CD solution that lets you automate building, testing and deploying your projects. It defines your objective as a workflow that you can independently setup and execute by running a bunch of commands on the cloud.

Semaphore Is Intuitive Semaphore provides an intuitive way to configure your CI/CD pipeline. It gives you a drag and drop interface that comes in handy for developers who’re not comfortable with writing custom configurations or YAML files for their projects. This removes technical barriers for developers to adopting CI/CD for their projects.

Semaphore Is Feature Packed Semaphore comes with features that make it great for small as well as large projects.

For instance, if you’re a startup with a small and growing project, you can create a single pipleline to run your builds in a sequential fashion. But if you’re building at scale, you can still use Semaphore to run multi-stage CI piplelines in parallel.

It also supports a large variety of tech stack, so it doesn’t matter if you’re deploying a NodeJS server or running integration tests for your Android build. It also supports caching and monorepos out of the box.

Interestingly, Semaphore has also pointed out how it helps you minimize the cost of developer productivity. You can check it out here.

What Is Bazel? Now that you have an idea of what Semaphore is and how it can be helpful, let’s understand what’s Bazel and how it fits into the picture. Bazel is a popular open-source build tool that allows developers to build and compile large monorepo projects efficiently.

Highlights of Bazel Some key features of Bazel includes reproducibility, high performant build process and support for multi-language dependencies.

It’s reproducibility allows developers to easily debug their build processes. It allows you to generate concurrent builds in parallel and uses a caching mechanism to intelligently compare what has changed in your code.

This makes your build process high performant at scale, because you’re only rebuilding code that has changed.

How Bazel Works At it’s simplest, Bazel takes a set of inputs, ie your code. Then, it produces some outputs, ie a build file.

However, under the hood Bazel follows four simple steps:

  • First, you inform Bazel about the settings or rules of your build via a BUILD file.
  • Then, Bazel loads this file at runtime and uses these specified rules to produce build actions.
  • Next, Bazel executes these build actions based on the inputs you specified for your build process to produce build outputs.
  • Lastly, Bazel stores the results of this process in cache artifacts for speeding up your future builds.

If you wish to dive deeper into what Bazel is, how it works and how you can build a JavaScript monorepo with it, I have a full fledged guide on it here. However, there we build our Bazel project on our own local machines.

In this tutorial we’ll take that project and use our CI/CD provider Semaphore to run this build on the cloud instead.

Essentials of Semaphore First, we need to understand some essentials of Semaphore. We know that Semaphore helps us manage, build, test and deploy our workflows. But first, let’s understand what a workflow exactly means.

What Is a Workflow? A workflow represents one or more tasks that you wish to perform. Further, it comprises of all the steps you’ll need to execute to accomplish that task.

For instance, you need to run some unit tests for your application. Or maybe you need to deploy your code on a staging environment.

Or maybe you need to do both, one after the other in a sequential fashion. All of these represent a workflow for your project.

Each workflow can be broken down into three chief components – Pipeline, Blocks and Promotions. Let’s see what each of these mean.

Pipeline A pipeline defines a series of steps you’d need to perform in order to execute a workflow in it’s entirety, or a part of the workflow.

Let’s take the case where you want to run unit tests for your application. In order to run your unit tests, you’ll need to pull your latest code from Github, execute a command that runs the tests, perform a code freeze if your tests are successful or roll back to another version or commit if your tests fail.

In order to complete these set of subtasks, you define a pipeline that encompasses all these steps.

Now let’s say you need to deploy your code to a staging environment. Again, you’ll create a pipeline that will include all the steps that will lead to successfully deploying your code to a staging environment.

In both the above cases, your workflow has a single pipeline. But take the case where you need to do both, in a sequential fashion.

So now if your tests are successful, you also need to deploy your code to a staging environment. In this case, your workflow will now have two pipelines, one that takes care of your tests, and the other that takes care of your deployment.

Blocks Each step or subtask in a pipeline is referred to as a block in Semaphore. So essentially when you run a pipeline, you’ll be running your individual blocks. Usually in a pipeline, blocks run one after the other, in a sequential fashion.

There may be scenarios where blocks run in parallel. For instance, you could have a block that checks the version of your testing dependencies against a list of recent versions. You could run that block in parallel to running unit tests, since these blocks don’t depend on each other.

Promotions Promotions in Semaphore are special blocks that connect different pipelines together. For instance you could use promotions to distribute a single build across different deployment environments. This could either be done inside a single pipeline or via multiple pipelines.

We previously discussed the example of a workflow where we have two pipelines – one for running unit tests and the other for deploying our code to a staging environment. These pipelines can be connected together via promotions.

Job and Tasks A job or task simply represents one or more commands that need to be executed in a block. For instance, here’s a simple command to install your project’s dependencies via npm:

npm install

The relevant block for the above job could be Installing Dependencies for your Project.

Configurations Everything in your workflow, your pipelines, your blocks, promotions and jobs are defined by a special configuration file written in YAML.

Your configurations basically govern how your workflow will look, what pipelines it will have, any promotions you should setup, etc. It also dictates what each individual block does and lists all the jobs that need to run for executing that block.

We’ll look at some additional configurations like prologue, environment variables etc as we configure our Bazel build on Semaphore.

Setting up Semaphore Enough theory, let’s get to the topic in hand now! First, we’ll need to setup Semaphore for our use.

Create an Account/Login Head over to Semaphore and click Sign up with Github. If you’re an existing user like me, click on Login.

You’ll land on a Login with Github page so you’ll need to do that next.

Once you’re inside Semaphore, if you’re a new user, you’ll need to create a new organization to start building some projects.

Next, you need to create a new Semaphore project.

Create a New Semaphore Project You’ll land on your Semaphore Dashboard, where you’ll need to create a new project.

Connect Bazel Project with Semaphore Now, you’ll need to connect your monorepo project’s Github repository with your Semaphore project.

I assume that you already have a Bazel monorepo sitting somewhere in your Github, but if you don’t, feel free to fork this one for this tutorial. If you want to learn and understand how we built this JavaScript monorepo project with Bazel, you can read about that here.

As a next step, select the repository that you just forked and click Choose.

At this point, you’re done with the initial setup. If you head back to your Semaphore dashboard, you should see your newly created project on Semaphore:

Create a Bazel Build Workflow on Semaphore Now that you have a project, click on Edit Workflow. Our workflow will have a single pipeline with multiple blocks that will run in a sequential fashion. Here’s how our workflow will look like by the end:

First, we need to setup our environment variables.

Configure environment variables In most CI/CD pipelines, you have to setup your environment variables that you locally use via a .ENV file. These may have the base URL you’re using for your REST APIs, any API secret keys etc. For our workflow, we need to setup some simple environment variables that pertain to a NodeJS runtime environment.

On the right panel of your workflow builder, scroll down to the Environment variables section. Next, add the following environment variables as shown:

Now let’s create blocks of our pipeline that would execute our workflow.

Create Blocks in Semaphore Semaphore would have already created a block for you to begin with, but we’ll go ahead and remove it since we want a clean slate to begin with.

Setup Bazel Block We’ll first create a block that will install Bazel. Let’s name this block Setup Bazel. You can do so by editing the Name of the Block field on the right side.

Next, we’ll specify the job this block will perform. Our job will install Bazel and also print it’s version just to ensure we successfully installed Bazel. On the right panel, if you scroll down you’ll see a section named Jobs.

We’ll name this job Install Bazel and Check Version. This job will run two commands sequentially:

npm install -g @bazel/bazelist bazel --version

The first command installs Bazel on the cloud globally. The second command checks the version of the Bazel that has been installed.

Install Dependencies Block Next, we need to install some other JavaScript dependencies for our project via npm. We’ll name this block Install Dependencies. It will have one job, that we’ll also name Install Dependencies that would simply execute the following command:

npm install

Here’s how the block and it’s job should look like:

Almost there! Let’s move to our final block where we’ll build our monorepo via Bazel.

Build Bazel Block Finally, our last block will run our build command that would tells Bazel to build our monorepo. Let’s name our block Build Bazel which will execute a single job called Build Project that runs the following command:

npm run build

Here’s how the block and it’s job should look like:

Add Prologue Prologue in Semaphore simply represents a set of commands you want to run before you execute each block. Since all our blocks depend upon a NodeJS environment, we need to ensure that we have that up and running.

Scroll down to the Prologue section. Then, add the following commands:

checkout node --version npm --version

The first command moves to the directory where our project is present and pulls up the latest code from the repository. Then, we simply check the version of node and npm installed to ensure that our NodeJS environment is up and running. In case any of these commands fail, our block will not execute at all.

It’ll also be easier to detect the bottlenecks in our workflow, since we’d know that our prologue commands are failing. Here’s how the Prologue section of your workflow builder should look like:

Translating Workflow to a Configuration File Up until this point, we have used Semaphore’s visual workflow builder to create our workflow, blocks and list our jobs. Under the hood, Semaphore translates these configurations in a semaphore.yml file.

Right above your workflow, you’ll be able to see that you can also edit these configurations by directly modifying your semaphore.yml file:

Click on the file and you’ll see how Semaphore has translated the workflow builder into it’s relevant configurations. If you’re familiar with building CI pipelines this way, you can directly write your configurations inside this file. In that case, Semaphore will do the opposite, that is generate the workflow builder for you based on your semaphore.yml file.

Ensure that your semaphore.yml file has the following configuration as well:

version: v1.0 name: JavaScript Monorepo Bazel Build agent: machine: type: e1-standard-2 os_image: ubuntu1804 ...

In the above we simply specify the name of our workflow and the configurations of our remote server that would run our Bazel build on the cloud.

Running Our Workflow Now that we’re done with building our workflow, let’s run it so Semaphore can actually help us run a Bazel build on the cloud.

On the right corner, you need to click the Run the workflow button. Semaphore will create a new commit pertaining to your workflow’s configurational changes. It will then push that commit to a separate branch called setup-semaphopre.

Let’s run the workflow now.

Now you’ll be back to your project’s page, where you’ll see each block being executed by Semaphore. If you’ve followed me until this point, all your blocks should run with flying colors!

You can click on each individual block to also monitor the logs. Let’s do that for our Setup Bazel block:

Notice how we get back the version of Bazel and npm on the job log console. In case you run into an error, these logs will help you detect bottlenecks in your workflow so you can fix issues in your process.

Lastly, if you visit your repository, you should see a new branch setup-semaphore with some commits:

Essentially, the setup-semaphore branch offers continuous integration for your project. This means that any new changes pushed to this branch will automatically run our pipeline and run a new Bazel build on Semaphore.

Note: To successfully execute our workflow, I made sure that our Bazel project doesn’t have a .Bazelversion file. This file may lead to a version conflict with the Bazel you install globally leading to an error when you run your Bazel build. If you need a .Bazelversion file locally, a smart solution would be to create a block in your workflow that deletes this file before setting up our Bazel build.

Conclusion There’s a lot more you can explore on Semaphore from this point onwards. You can learn how to cache result of our Install Dependencies block here. Semaphore has a dedicated section in their docs towards building a generic Monorepo workflow. So if you’re using something other than Bazel for your Monorepos, it would bring some great insights to get you started.

The post Run Bazel Build on Semaphore appeared first on Semaphore.

View Details

In this podcast episode, I welcome Kris Buytaert, consulting CTO at Inuits.eu, DevOps evangelist, one of the organisers of DevOpsDays. We talk about the conference, how to introduce CI/CD to teams, and what are some patterns and antipatterns for infrastructure as code. We also discuss why teams are reluctant to spend money on testing and operations, and what happens if they don’t.

Listen to the full conversation or read the edited transcript.

Table of contents:

  • Introduction
  • The reality of conferences in COVID times
  • Introducing CI/CD to teams
  • Infrastructure as code: patterns and antipatterns
  • Drawbacks of tech education
  • Why do organizations not spend money on testing?
  • How to get budget for DevOps

You can also get Semaphore Uncut on Apple Podcasts, Spotify, Google Podcasts, Stitcher, and more.

Like this episode? Be sure to leave a ⭐️⭐️⭐️⭐️⭐️ review on the podcast player of your choice and share it with your friends.

Edited Transcript Introduction Darko Fabijan:
Hello, and welcome to Semaphore Uncut, a podcast for developers about building great products. Today, I’m excited to welcome Kris Buytaert. Please go ahead and introduce yourself.

Kris Buytaert:
I’m Kris, I live in Belgium. About 20-25 years ago, I started playing with Linux and realized that open source was what I wanted to do. I started out as a developer doing projects in Java and other languages.

As I progressed in my career, I started doing more operational work because I knew how to make clusters and work machines. Some 12 years ago, I spoke at a lot of conferences on how we did high availability and scalability and automation, etc.

I bumped into Patrick Debois at a CloudCamp in Antwerp. He had this crazy idea to bring together 70 best of our friends to a small conference and get both operational background, HL background, cloud people, and developers in one room. That’s how we started DevOpsDays in 2009.

Since before that, I’ve been helping organizations to deliver software in a better way using open source tools.

The reality of conferences in COVID times Darko Fabijan:
Great introduction! What’s the reality of DevOpsDays in COVID times?

Kris Buytaert:
My last DevOpsDays was New York, 2020. I haven’t been to one since, I went to OSMC beginning of the month, in Nuremberg.

Online conferences are really not the same as in-person. The real power of DevOpsDays was the open space, getting to talk with people who have similar experiences, who can share ideas. It’s hard to mimic online.

There are still DevOpsDays popping up left and right; some of them happen in-person. One happened in Tel Aviv recently, and a lot of people were happy to attend it in person.

But there’s DevOpsDays that have been postponing 3 or 4 times already. It’s not easy.

Introducing CI/CD to teams Darko Fabijan:
I couldn’t agree more: the best part of any conference is when you get to talk to a bunch of people.

One of the main things that you wanted to achieve with DevOpsDays is to bridge the gap between operations, DevOps, agile, and structuring work. You gave a lot of talks on how to introduce teams – DevOps, development teams – to CI/CD as a topic.

Can you give us an overview, what’s the best practice?

Kris Buytaert:
A lot of the work I do is consulting, which means I end up walking into organizations that are struggling, that are failing, and that actually need help. Which does mean, in a lot of cases I only see the shit that’s around.

But people are struggling because they have a problem they need to solve. They cannot get to production at the speed they want, they have stability problems in production. Basically, they have problems delivering software.

Oftentimes, you talk with different people in the teams and realize that their whole effort into trying to deliver software hasn’t included operational people. It has just been developers doing some testing, then building a pipeline. They’ve been doing continuous integration but how it’s being delivered is totally ignored by the rest of the team.

On the other hand, there’s successful transformations. It’s the ones where operational people were involved, and they we’re upfront willing to automate faster, build things. They want to be able to support their developers teams.

Then you tell them that you’re going to do infrastructure as code and continuous delivery on their infrastructure. You’re going to teach concepts like how to do promotions, test coverage, all those things. When you get teams to understand that, it’s much easier for those people to support their developer teams.

When you get teams to understand concepts like infrastructure as code and do continuous delivery on their infrastructure, it’s going to be much easier for those people to support their developer teams.

-Kris Buytaert, consulting CTO at Inuits.eu

Kris Buytaert:
The trick to get more people of an SRE style role, is to understand and learn those things. Your CI/CD infrastructure needs to also be something where you can do continuous delivery on. You should be capable of constantly upgrading, knowing that you can release at any point in time.

When people become fans of the whole ecosystem, then they start to understand, like, “Yeah, this is how it works, we can show it to other people”. That really helps organizations forward but sometimes you come into an organization way too late for that.

Infrastructure as Code: Patterns and Antipatterns Darko Fabijan:
Six or seven years ago, infrastructure as code was becoming a reality for more teams. If you compared those times with today, what are some patterns and antipatterns? How easy is it to get people to jump on this train?

Kris Buytaert:
There was a fun thread on Twitter a couple of weeks ago where the CNCF kind of defined what GitOps was about a lot of the people that have been around doing infrastructure as code for ages were like, “Yeah, but this is what we’ve been doing for close to a decade and a half. We do desired state, we do version control, we do actually some tests on this.”

That was even missing in the CNCF definition, “We do test on what we we do, and then it gets deployed, and it stays in that state, and we were capable of reprovisioning things, we’re capable of doing all of this automated.”

The honest remark there is I haven’t seen a lot of organizations who are capable of doing continuous delivery on their infrastructure as code, but they do exist.

Kris Buytaert:
Sadly, what I see now, mostly people that are struggling is if they claim we do GitOps, they do continuous isolation, because they basically have multiple branches, scales, running all over the place. Then they have some operator deploying their state of the application in their container ecosystem.

In a way I understand where a problem space is, where they’re coming from. But that is absolutely the opposite goals of doing continuous delivery of your infrastructure. Because the clue and the really important thing is testing, the other part is we do trunk based development.

So, if you have multiple long running branches, you’re going to shoot yourself in the foot at some point. So, I think a lot of those things are circular movement, where people bump into a problem space. They look into ways how to solve them within that problem space, don’t know yet that within their own community, that there’s been people who solved similar problems before with other technologies.

Kris Buytaert:
A lot of those people are going like, “So, yes, we’ve solved this problem before, if you just take this, this and this pattern, and you reuse your tools the other way, you kind of have solved it.”

But they go out and they start building new tools, where they then each time do some incremental improvements, but sometimes forget about, hey, this already exists.

Those who don’t know Unix are doomed to reinvent it – poorly.

-Kris Buytaert, consulting CTO at Inuits.eu

The joke about it is those who don’t know Unix are doomed to reinvent it, and then a lot of people add poorly. The only thing we should look for is there are obviously always incremental improvements, but what they exactly? Why are people trying to solve a problem the way they currently solve it?

That is, to me, what’s really the important part, what is actually the problem you’re trying to solve?

Darko Fabijan:
Yeah, I have seen that, and actually talked with a lot of guests on the podcast about those cycles that we have in the industry. Do you think it’s also related to the generations of people coming into the industry, and not knowing what was done before and what is available?

Kris Buytaert:
It’s definitely to do with new people joining the industry, not knowing what exists, and it’s also getting impossible to know what exists. There are so many things moving, there are so many things popping up.

There is, however, a struggle where a lot of those people come into the organization and say, “Hey, we know how to do things,” and just forget about there’s other people also out there who have experience, learn things.

That definitely is important, that sometimes knowing the pattern and seeing what’s happening is much more important than knowing the specific tools, and figuring how to do such and such configuration with such and such tool. But we’ve seen over years that a lot of what is needed in organizations is really teaching people: we do these things because… We are going to implement this feature, because if we don’t, this is going to fail.

New and young people, in a way, don’t have that operational experience.

Kris Buytaert:
They haven’t seen things failed yet. That creates conflict because they think things are not going to fail. People with experience say, “Yes, but maybe hold your solution differently because you’re going to hurt yourself”.

It’s a challenge in both ways. On the one hand, the new people need to accept that input. On the other hand, the old people need to accept there’s obviously incremental improvements and they could learn from each other.

But it’s still a hard problem, preventing people from failing fast.

But it’s still a hard problem, preventing people from failing fast.

-Kris Buytaert, consulting CTO at Inuits.eu

Drawbacks of tech education Darko Fabijan:
Some analogy here. I’m trying to convince developers that tests are here to help and guard you. The reality is, almost 15 years since I graduated from the university, most universities are still not teaching testing. You can boast around the programming languages you know but you almost never wrote a single unit test.

Can you do some mapping how that maps to the DevOps world? How do you see education in this area of DevOps, what are good patterns?

Kris Buytaert:
You touched on a couple of things. People coming from university and having been taught GitOps, they basically fall into this gap like, “But this is not what they told us it would look like.”

The real world is different, yes, there’s people who are doing it this way, but that is not the majority they see. There’s already a gap between what the schools are teaching, because they want to do the new technology thing, and what they actually should be teaching.

The second gap is, you said they don’t cover testing and things like that, I can perfectly imagine that they don’t do, but they don’t even cover the operational part.

Kris Buytaert:
Most bachelor and master programs are focused on building things, not running things. So, there is a huge gap. I really don’t know a lot of educations where you can actually become site reliability engineering, or that kind of roles, they just don’t exist as far as I know.

If we’re educating people to a point where they become software developers, and their goal is to write code, and get that out there, then we’re doing something wrong with education.

The second part is, it’s not only education which is a problem, it also is how a lot of the software projects are being financed. If you ask the average business, they want functionality. If you give them a €100,000 budget, they’re going to spend €99,999 on functionality. Then they’re going to realize like, “We need to keep this maintained and up and running for the next couple of years.”

Why do organizations not spend money on testing? Kris Buytaert:
They realize they have one euro cent left to do so, and that’s going to be a problem.

Even the bigger problem is when somebody asks you, “Can you build this piece of software?”

You go explain them and say, “Sure, we can do this, we’re going to spend this amount of time into writing it, and this amount of time testing it.” They’re going to say, “No, no, no, no, don’t spend this amount of time testing it,” because they don’t understand how software deployment works.

They don’t understand that it is needed, and your testing budget from that point of view is always going to be under pressure, you’re never going to have sufficient budget to test it the way you want.

I haven’t even touched on security yet, that’s an even larger not existing budget, because it’s built in, it’s by default. That’s what the average organization expects, so not only schools and universities are not teaching people how to build software, and how to do the whole life cycle around it.

Kris Buytaert:
We also fail to teach the people that come out and that end up in the business that, hey, you need to think about than just building the code, and just having the functionality there. You need to talk about those non-functionals, you need to talk about how to do testing and how to automate your testing.

If you don’t do that, those people will not understand.

Still, on a daily base, you have customers and organizations internally struggling with budgets, because we’ve allocated this amount of money to build this, why do you need 10 times more? That’s the daily struggle.

So if you have that problem, schools are not going to tell you, “Hey, you need to teach testing,” because they see like, “Yeah, but nobody spends enough time on it, it’s not important enough for us to spend time on.”

Schools don’t teach testing because they see that nobody spends enough money and time on it.

-Kris Buytaert, consulting CTO at Inuits.eu

That kind of gets you in a situation where people are not going to be taught about it, because other people are not paying for it. It’s a sad situation.

Darko Fabijan:
Do you have any tips and tricks of managing upwards and helping engineers in organizations to get those budgets, or reassign those budgets to things that are important?

Kris Buytaert:
Yes and no. So the scary part is I’ve got a couple of cases where the actual transformation started happening after a near-dead experience. Senior management realized that they needed to start managing things differently, because if they kept going the way they were going, they would go out of business.

So, that kind of near-dead experience, like having experienced real pain and having seen the almost disaster level business impact, it’s a really good way to let people think about how to deal with test coverage, how to deal with resilience, how to deal with availability, and quality in general.

But, let’s hope that not every organization has to experience that near death thing. The thing that does work, however, is if you have product teams where the product owners are involved in creating the backlog and stuff.

Kris Buytaert:
That you put those people on call, because those are typically the people who forget to prioritize nonfunctional requirements, who forget to spend time on test coverage.

Once you get those people to be woken up at 3:00 AM on a Sunday morning, because there’s yet another out of memory issue, which the previous group of on-call engineers has been nagging about for at least a year, that it needs to be fixed because they’re being paged.

When that happens for the third Sunday in a row, that is when those product owners start realizing, “Why am I being paged for this? This needs to be fixed,” and I experimented with that concept with a couple of teams already a couple of years ago, six, seven years ago in the Netherlands.

Kris Buytaert:
We saw change happen, because you see those non-functional requirements, you see those bugs that are basically getting people out of bed, you see them eventually being solved.

That improves the quality, and that means that people are finally starting to realize like, “Well, there is a hidden cost for people being paged, there is a hidden cost for having to use much more resources than we should be using, because there’s resource allocation issues in our code.”

I think that is really a good tip, if you can get those people to be the ones who get woken up when there’s a problem, things might change.

If you can get those people who are being woken up at 3am because of a memory issue to realize that there’s a hidden cost for people being paged, things might change.

-Kris Buytaert, consulting CTO at Inuits.eu

How to get budget for DevOps Darko Fabijan:
Yeah. When do people change their diet? After they get a stroke or a heart attack, that’s the wake up moment of changing.

A question in this regard. As our teams hit a milestone, we end up rediscovering and reevaluating the budget for maintenance. Just today, we have been talking that we’d want to have a number of metrics regarding our databases or how systems are operating.

But they’re being pushed under the carpet for a long time. So we were talking, okay, how to allocate time, how to have those iterations every N weeks.

Every fifth week we have a budget for a week to focus on something. And then we do that over time, and then we have to reinstate that budget. What is your experience in that area?

Kris Buytaert:
I’ve seen the same, not with doing it every couple of weeks, but it’s really hard trying to say, “Hey, we’re going to spend 10, or 15, or even 20% of our budget into actually doing continuous improvements”.

Because the pressure from the product owners, that they end up pushing features, feature, features. You really have to have a strong commitment from your management, that you should be doing this, and once you have that, it often stays happening.

But then you get this big customer who walks in, and they get prioritized, and it changes again. And even then they get reprioritized.

So, I’ve yet to see organizations that can really do continuous improvement on their code base. It’s as you described in a lot of cases, it goes good for a couple of months, and then it sadly disappears again.

Darko Fabijan:
Yeah, so reinstating it, reinstalling the practice is just one of the ways to do it. You having such a vast experience in the DevOps area, looking forward, you mentioned SRE as a role. Which is to my understanding, kind of a newer role, which is in some organizations becoming more of a mainstream?

Kris Buytaert:
So, DevOps really never was a role, and the way Google has defined such reliability engineering, it’s pretty much what they claim is their implement of DevOps.

But, if you look at describing what somebody does, such reliability engineering has much better description of what most senior system engineers are doing, as opposed to telling that they’re doing a DevOps engineering role.

Which is still like, “What is a DevOps engineer?” Is that a Java developer who knows how to deploy code, or is that a Linux engineer who knows how to debug stack trace? It’s none of it to me, a DevOps engineer is not a role.

A DevOps engineer is not a role.

-Kris Buytaert, consulting CTO at Inuits.eu

So, in a way, if we talk about people in this industry, then business critical engineering, site reliability engineering has much more of an explanation of what people actually are doing or supposed to be doing.

Darko Fabijan:
Yeah, you put it nicely and got me thinking. It’s been a struggle with a lot of people that we have been talking to. We are talking about DevOps, and people are saying, “Hi, guys, meeting a customer, this is John from DevOps team.”

There is that struggle of understanding of what are his area of expertise, and what is he responsible for? It ends up varying vastly between teams.

Kris Buytaert:
So, when organizations come to me and they say, “We want to do this DevOps thing,” what I typically tell them is don’t call it the DevOps project, call it we’re going to do engineering whatever. Then cherry pick from all the things you’ve seen, what you want to achieve as an organization, and throw that out as a plan.

People might end up saying, “Hey, well, this is DevOps. Okay, good.” But if you say, “Hey, we’re going to do this DevOps thing,” what you’ll end up is six months of discussion about what does that really mean?

Whereas, you say, “We’re going to do faster delivery,” that you have a goal set, these, these, and these are the things we want to achieve.

Those are the steps and you’re on a journey as an organization, and that is basically what DevOps is about, improving yourself, improving quality of your software delivery there.

But, setting out that goal as this is our organizational role, and we call it this, it’s going to be much more struggle less than saying, “We’re going to do DevOps.”

Darko Fabijan:
Yeah, thank you, Kris. These are very nice closing lines, the explanation, thank you very much for your time. We all hope the DevOpsDays and all other live events we’ll resume soon, but all we can do is keep our fingers crossed. Thank you again so much for your time.

Kris Buytaert:
You’re welcome.

The post How to Introduce Your Engineering Team to CI/CD with Kris Buytaert appeared first on Semaphore.

View Details

Deploying iOS apps to early testers helps you get feedback for improvements or detect issues that came out from early on during the development. Firebase App Distribution, or App Distribution, is a service that helps you deploy your Flutter apps to testers.

This article focuses on automating deployments for Flutter (iOS) apps using ad-hoc releases in App Distribution. If you’re interested in implementing this for Android, you can read this.

Prerequisites * An existing Flutter project; you can use our starter app or create one by running flutter create my_app * A Firebase account; you can create one here * An Apple Developer Account ($99 / year) for the developer certificates and provisioning profiles

Firebase App Distribution Deploying iOS apps is more complex than in Android. Unlike in Android, most of the time, you can create an android package (APK), and users can directly install and use them on their devices. But for iOS, it’s a different story. You can only install apps from the Apple Apps Store or signed ad-hoc releases from tools like App Distribution.

App Distribution and Apple Apps Store

App Distribution is an alternative service to deploy iOS apps to a limited number of testers to get early feedback or detect issues, and it’s not a complete replacement for the Apple Apps Store if you are planning to release your apps to the public.

Creating a Firebase Project In the console, create a new project by tapping Add project.

Assign a project name and unique ID (optional).

You can also enable Google Analytics (Optional). For now, we’ll disable this setting.

Authentication with Firebase CLI Firebase CLI contains tools that allow you to manage, view, and deploy to Firebase. We will use Firebase CLI to authenticate CI later when deploying builds to Firebase projects.

Setting up Firebase CLI

There are at least two ways to set up Firebase CLI on your machine. Either by downloading the standalone binary or via the npm package manager.

If you already have npm, you can go ahead and install Firebase CLI using the package manager, as shown below:

npm install -g firebase-tools

Testing and logging in with Firebase CLI

Authenticate Firebase CLI using the email associated with your Firebase account:

``` firebase login

✔ Success! Logged in as joshua*@gmail.com ```

For more information, see Firebase CLI reference.

Setting up App Distribution App Distribution in Firebase is where deployed builds show up and where you can manage your app testers.

Setting up an iOS project

If you have an existing Flutter project that uses Firebase, please skip this step. Otherwise, do the following if you are using the starter app or have created a new project using flutter create.

In the Firebase console’s sidebar, navigate to and open App Distribution.

Tap the iOS logo to create a new iOS project.

Then, enter the details of your iOS project. Optionally, you can download the GoogleService-Info.plist file and put them to your root iOS project directory.

If you use the starter app, replace com.example.semaphoreciFlutterDemo with your desired bundle identifier.

Setting up group testers

App Distribution allows you to configure multiple test groups depending on the build you deploy, for example, for internal or private beta builds.

Navigate to the App Distribution page and select the Testers & Groups tab.

Add a group named “testers”, then click Save.

Last, to deploy your build to the users, add their emails to the group.

fastlane fastlane is an open-source platform with a wide range of tools and APIs for simplifying deployment for Android and iOS projects. We will use fastlane to automate our workflow, communicate with Firebase, and ultimately deploy the project.

Setting up Ruby

Ruby is a must-have to use fastlane in your project. Here’s an exhaustive list of ways to install Ruby on your machine.

Setting up Bundler

Bundler and Gemfile are for defining dependencies in fastlane. Run the following command to install bundler:

gem install bundler

Setting up fastlane

If you’re on macOS and have Homebrew installed, run the following:

brew install fastlane

If you have gems installed on macOS/Windows/Linux, run the following:

sudo gem install fastlane

Setting up fastlane with your Flutter iOS project Set up fastlane in the ios directory of your Flutter project.

To initialize fastlane, run the following:

cd ios && fastlane init && cd -

  1. Select manual setup:

``` What would you like to use fastlane for? 1. 📸 Automate screenshots 2. 👩‍✈️ Automate beta distribution to TestFlight 3. 🚀 Automate App Store distribution 4. 🛠 Manual setup - manually setup your project to automate your tasks

4 ```

Then, wait for all the packages and dependencies to be installed properly.

You should now have a fastlane directory containing the Appfile and Fastfile.

Adding the Firebase App Distribution Plugin to Fastlane We will need the plugin to upload the build artifacts to Firebase.

Install the fastlane plugin with the following command:

fastlane add_plugin firebase_app_distribution

Then, when asked to modify the Gemfile for the current path (ios), type y.

Depending on your Ruby setup, you might be asked to enter your password and provide temporary root privileges when installing the dependency:

sudo fastlane add_plugin firebase_app_distribution

Setting up Fastfile with Firebase Fastfile

Fastfile stores the configuration for automation using fastlane.

First, navigate to your Fastfile directory and replace the default content with the following:

``` default_platform(:ios)

Credentials (for later)

git_authorization = "" firebase_app_id = "" firebase_cli_token = ""

Team

team_id = ""

platform :ios do desc "Deploy iOS to Firebase" lane :deploy do # add actions here: https://docs.fastlane.tools/actions end end ```

deploy here is the name of the fastlane workflow. To run the command in your terminal, run fastlane deploy.

Next, below team_id variable, add the following:

```

Keychains

keychain_user = "temp" keychain_pass = "temp"

def delete_temp_keychain(name) delete_keychain( name: name ) if File.exist? File.expand_path("~/Library/Keychains/#{name}-db") end

def create_temp_keychain(name, password) create_keychain( name: name, password: password, unlock: false, timeout: 0 ) end

def ensure_temp_keychain(name, password) delete_temp_keychain(name) create_temp_keychain(name, password) end ```

These are for creating temporary keychains and downloading developer certificates, and provisioning profiles on the CI.

Then, inside the deploy lane, add the following:

``` platform :ios do lane :deploy do ensure_temp_keychain(keychain_user, keychain_pass)

gym(
  scheme: "Runner",
  archive_path: "./build/Runner.xcarchive",
  export_method: "ad-hoc",
  output_directory: "./build/Runner",
)

firebase_app_distribution(
  app: firebase_app_id,
  firebase_cli_token: firebase_cli_token,
  release_notes: "Bug fixes and performance improvements",
  groups: "internal",
)

delete_temp_keychain(keychain_user)

end end ```

App ID

To get your App ID, click Project Overview, then Project Settings.

Next, select the current app’s Project Settings and copy your App ID.

Replace firebase_app_id in your Fastfile.

Generating Firebase CLI token

Earlier you authenticated Firebase CLI with your Firebase account; mainly for testing tooling installation. Now, we will authenticate and generate a token for CI.

To generate a token for CI run:

firebase login:ci

This should give a hashed string like this:

``` ✔ Success! Use this token to login on a CI server:

1//0etIq08H_oIB5CgYIARAAGA4SNwF-L9IrgmGb2iadCGcAmqhzK_NH8F8zQBkbPrMS2qtWhMqUmpbVbYK4YzC_83-eHb8NgylEMUg ```

Replace firebase_cli_token in your Fastfile.

Setting up fastlane match fastlane match, or match, simplifies the complex code signing for your iOS apps, and Semaphore has an in-depth documentation on how to deal with iOS code signing using fastlane match. I will explain the setup here succinctly.

Configuring match

Initialize match by running

fastlane match init

Next, select git to store the developer certificates and provisioning profiles:

``` fastlane match supports multiple storage modes, please select the one you want to use: 1. git 2. google_cloud 3. s3

1 ```

Then, enter the URL of your git repository, for example: [https://github.com/joshuadeguzman/ios-certificates](https://github.com/joshuadeguzman/ios-certificates)

Finally, replace development to adhoc in your Matchfile

type("adhoc")

Generating certificates and profiles

Create ad-hoc certificates and profiles by running. When prompted, enter your Apple Developer Credentials.

``` fastlane match adhoc -a

...

All required keys, certificates and provisioning profiles are installed 🙌 ```

Use the match provisioning profile in your project settings using Xcode:

Downloading certificates and profiles in fastlane

In most cases, you should be able to clone your match’s git private repository on your local machine. But in CI, we have to provide appropriate authorization to allow access.

Create your Github personal access token by following this.

Use the personal access token generate and replace the git_authorizationin yourFastfile:

git_authorization = "joshuadeguzman:<YOUR_ACCESS_TOKEN>"

Last, add the match command

``` platform :ios do lane :deploy do ensure_temp_keychain(keychain_user, keychain_pass)

# Add this
match(
  type: "adhoc",
  git_basic_authorization: git_authorization,
)

... (truncated)

end end ```

Deploying to Firebase using fastlane locally That’s it for Firebase and fastlane for now. Let’s test the Firebase deployment by running fastlane locally.

Ensure the project is in clean state:

flutter packages & clean

Then, build the Flutter iOS app:

flutter build ios —-no-codesign

To deploy your app, run the following:

cd ios && fastlane deploy && cd -

This should give you a success message for deploying your app to Firebase.

Great! Your latest iOS build should be displayed on App Distribution’s releases dashboard.

Deploying to Firebase using Semaphore Before proceeding, I recommend reading core concepts that make Semaphore intuitive and powerful. I also explained the Visual Builder for iOS continuous integration in this article.

Setting up a Semaphore project for your app Log in with your Semaphore account, then create a new project.

Choose the repository for your project, then click Continue to workflow set up.

Then for the workflow set up, click Customize.

Setting up blocks to integrate and build your apps Configuring the workflow pipeline

We’ll be using a Mac Based Virtual Machine environment with a macos-xcode13 image.

Setting up your blocks for Continuous Integration (CI)

Create a block named Install Dependencies, then add a job Install and cache Flutter:

checkout cache restore flutter-packages-$SEMAPHORE_GIT_BRANCH-$(checksum pubspec.yaml),flutter-packages-$(checksum pubspec.yaml),flutter-packages flutter pub get cache store flutter-packages-$SEMAPHORE_GIT_BRANCH-$(checksum pubspec.yaml),flutter-packages-$(checksum pubspec.yaml),flutter-packages /root/.pub-cache

)

Then, add a new block named Lint, then add jobs Format and Analyze:

Format

flutter format --set-exit-if-changed .

Analyze

flutter analyze .

Prologue

checkout cache restore flutter-packages-$SEMAPHORE_GIT_BRANCH-$(checksum pubspec.yaml),flutter-packages-$(checksum pubspec.yaml),flutter-packages flutter pub get

Finally, add a block to run your Flutter tests.

Run unit and widget tests

flutter test test

Prologue

checkout cache restore flutter-packages-$SEMAPHORE_GIT_BRANCH-$(checksum pubspec.yaml),flutter-packages-$(checksum pubspec.yaml),flutter-packages flutter pub get

Optionally, you can test your workflow by clicking Run the workflow.

Configuring promotions to deploy your apps Setting up a new promotion

Promotions help us create conditions around our workflows and whether or not we have to run another workflow or set of workflows. Also, they can be triggered manually or automatically.

After configuring the visual builder, we can merge the set-up-semaphore branch to the default branch for continuous deployment to Firebase using automatic promotions. In our case, the Deploy to Firebase promotion gets triggered once the condition "branch = 'master' AND result = 'passed' is satisfied by the workflow run.

Configuring the deployment pipeline

Similar to the Main pipeline, we will use a Mac Based Virtual Machine environment with a macos-xcode13 image.

Next, in this pipeline, you will also need a Install Dependencies block set up with a job Install and cache Flutter:

checkout cache restore flutter-packages-$SEMAPHORE_GIT_BRANCH-$(checksum pubspec.yaml),flutter-packages-$(checksum pubspec.yaml),flutter-packages flutter pub get cache store flutter-packages-$SEMAPHORE_GIT_BRANCH-$(checksum pubspec.yaml),flutter-packages-$(checksum pubspec.yaml),flutter-packages /root/.pub-cache

Last, create a block named Deploy to Firebase with a job Run Fastlane:

checkout cache restore flutter-packages-$SEMAPHORE_GIT_BRANCH-$(checksum pubspec.yaml),flutter-packages-$(checksum pubspec.yaml),flutter-packages sh bump_version.sh flutter build ios --no-codesign cd ios bundle install cache store bundle exec fastlane deploy

This block is responsible for executing your deploy lane in fastlane.

Click Run the workflow and commit the changes.

Awesome, your workflow should be ready for automatic deployments.

Testing continuous deployment

To check automatic promotions and deployments, we should merge it to the working branch to master:

git fetch --all git merge origin/set-up-semaphore git push origin master

Now, push the changes to trigger the build.

Conclusion Setting up a continuous deployment pipeline for iOS takes more time than Android, but it’s worth the investment. Creating new releases every time a developer merges a pull request using this approach saves you half the time if you’re doing all the steps mentioned above manually, especially dealing code-signing for your teams.

The post Deploy Flutter (iOS) Apps to Testers using Firebase App Distribution with Semaphore appeared first on Semaphore.

View Details

In this podcast episode, I welcome Jan Giacomelli, Staff Software Engineer at Ren Systems, previously with typlessAPI. We talk about test-driven development, unit tests, flaky tests, and much more. Jan shares some exciting war stories from the trenches of testing, explains how he was able to reduce the feedback loop from 45 minutes to only 8 min, and advocates for TDD. Listen to our insightful conversation or read the transcript.

Key points:

  • The benefits of TDD and good test coverage
  • TDD at typless
  • TDD: patterns and anti-patterns
  • The problem of testability
  • Unit tests
  • Flaky tests

You can also get Semaphore Uncut on Apple Podcasts, Spotify, Google Podcasts, Stitcher, and more.

Like this episode? Be sure to leave a ⭐️⭐️⭐️⭐️⭐️ review on the podcast player of your choice and share it with your friends.

Edited Transcript Darko Fabijan (00:02):
Hello, and welcome to Semaphore Uncut, a podcast for developers about building great products. Today, I’m excited to welcome Jan Giacomelli. Jan, thank you so much for joining us.

Jan Giacomelli (00:12):
Yeah, thank you for inviting me.

Darko Fabijan (00:14):
Great. Please, just go ahead and introduce yourself.

Jan Giacomelli (00:18):
Yeah, my name is Jan, and I’m senior software engineer. I work mostly with Python in AWS for the last seven, eight years. I’m a huge advocate for test-driven development, and in the last years, I also did a lot of work building and optimizing CI/CD pipelines on GitLab.

I’m very proud that I managed to bring our pipelines from 45 minutes from push to production with included manual testing to around eight minutes without any manual testing. So everything just goes to production, every little change, as little as it is.And since then we hadn’t taken out of production any point.

I’m just starting a new role, so the story is beginning one more time. So I just set up a new GitLab pipeline. We did a migration from GitHub. And now our pipelines are running around 30 minutes, but I know where to start a journey to bring them below 10 minutes.

The Benefits of TDD and Good Test Coverage Darko Fabijan (01:26):
Great. Yeah, fast feedback loop is super important. That has also been our obsession for almost a decade now, and great that you have such wonderful results. I mean, keeping it under 10 minutes is definitely super important.

To jump to the beginning, I think the vast majority of our listeners are on board with the idea of TDD and why testing is important. Maybe you can run us through couple of points that you see as most important in your day-to-day life and life of your teams. What does TDD and generally good test coverage bring?

Jan Giacomelli (02:09):
One very important thing for me is to develop in very small steps to introduce very small changes which can be integrated in a later steps to a successful and working solution that will solve the problems of our customers. So I’m a very busy guy, so my phone is ringing all the time, my emails are incoming all the time, Slack messages.

And yeah, I can mute them, but at the end of the day, if I need to collaborate with my team, I try to be available as much as possible. So one of the goals when I started programming as my living, as my full-time job, was to be interruptable, so to be able to switch context quickly, to not wait for something very long in order to see whether it is working or not, or just spending a lot of time figuring out where was I before someone asked me something.

Jan Giacomelli (03:08):
So yeah, test-driven development definitely helped in this matter because if you develop in very small steps and if my work before the interruption was 10 minutes long or 15 minutes long, it’s very likely that when I go back and I will open my IDE, I’ll be able to wrap my head around in a second because, “Oh yeah, I was doing that, and that test is still failing, so I needed to make sure it passed.”

TDD helps you to work in smaller iterations and be able to wrap your head around what you were working on before you got interrupted

And another great motivation for me was to have a system that’s working at all times. So I was really struggling because there were some bugs to fix or some new features to add, and I did some coding, I did some manual testing. But it took quite some time because I had to spin up local server and then user interface and then to click across application. And then I forgot to check some of the cases which were working before. I pushed that, someone reviewed that, which is probably in hurry, and then broken code went to at least staging if not even to production.

TDD at typless Jan Giacomelli (04:13):
And when we started typless in 2017, that was our daily job to take down the production as soon as possible. Every change almost introduced us something breaking, and it was super frustrating.

So I started to research and to try to find a way to prevent this from happening and to spend the least amount of time possible on a problem, because it may seem that you develop fast if you don’t write tests, but then you push that, then you need to wait for someone to check that this is working and then it goes to production and someone realize, “Oh, this is not working as expected.”

And then you receive a bug fix. But yeah, that’s a new ticket. So we think, “Oh, I closed the previous one last week and now I have to deal with this one. This is actually the same.” And then you break something else because there is no behavior cover with tests because maybe there was a requirement to have a 100% of test coverage, but someone just added a certain not none for the assertions, and you actually broke something but tests are still green.

Jan Giacomelli (05:23):
So many of ways in situations in which you can find yourself. After five years of practicing test-driven development, it’s been a year since I can say, Okay, I’m 90% or even more faster by doing test-driven development than without it, because it’s just under my skin. That’s the way how I see things. That’s the way how I verify them. That’s the way how I think about things.

I’m 90% faster by doing test-driven development than without it

-Jan Giacomelli

Everything needs to be testable, whether it’s just a requirement and I need to be able to verify that this requirement can be satisfied because there may be some logic gaps and I don’t even need to write a test. I can just think about what the test would be and I can be like, “Oh my God, I can’t do that because this was lead to a race condition,” for example, or, “This user cannot be in status of deleting and onboarding at the same time. I either need to wait for onboarding to finish to start deleting, or I shouldn’t delete it because it’s onboarding,” and stuff like that.

Jan Giacomelli (06:29):
Yeah, it helped me a lot, but it was very frustrating, especially at the beginning, because I didn’t know how to write tests. There is also I a huge mess in resources about the terms, what is unit test, what is integration test, what is test double, what is mock and stuff like that.

So it was really hard to find a way to test behavior, to test things in a way that my test are resistant to refactoring, so that actually that’s just desired behavior, not every little function or a method in my system, because it doesn’t make sense, I need rewrite it or split it into three, or I will find some abstraction and I can introduce a new class, a new function, a new module, whatever. Doesn’t matter.

I want to make sure that my business rules are still followed, that my client can still, for example, delete all of its data because it’s required by GDPR. It’s a functionality that must not be broken at any point in time because I can pay a huge fine if I don’t meet those requirements and stuff like that.

TDD: patterns and anti-patterns Darko Fabijan (08:16):
Something that you touched upon and I really want to ask more about that is you spent a decade in Python and helped a lot of engineers also onboard them into the world of TDD and generally writing applications by having them well tested by engineers who are also developing them. And you mentioned struggling with resource and terminologies and all that.

So it would be great if you could give us some patterns that you have seen, and anti-patterns, with a general approach of onboarding people to the whole concept, but also maybe once they are living with that for some number of years, some patterns and anti-patterns that you have seen that work in practice.

Jan Giacomelli (09:01):
Yeah, so one thing that’s very common in Python is that when the new functionality is required, then is only one if statement or one else statement added to an already completely too long function, which may take 400 lines of code, because there is no static typing, basically anything is working.

And as you can imagine, it’s very hard to test such function because it may contain 25 different behaviors which are to some degree independent of each other, but at the same time, because of the co-structure, they are not really independent of each other.

So that’s sure one anti-pattern that I see a lot. I haven’t seen that much of, let’s say, Java code bases, although I’ve seen all sorts of stupid things done in Java code bases as well, but I would say there is-

Darko Fabijan (09:59):
I can tell you, there are. There are awful things there, too.

Jan Giacomelli (10:03):
Yeah, yeah. The example, we required 100% test coverage and a cert not now was from one job project I was working on. So yeah, I’ve seen many… But I’ve seen way more Python code comparing to Java, for example.

Long function Darko Fabijan (10:18):
Concretely about that. I mean, long function, that’s like a very standard thing in our practice. Have you guys over time worked with some linked in tools that essentially maybe limited the length of the function or in some way complexity of a function?

Jan Giacomelli (10:39):
Yeah, so we use Flake8, which has… I think it’s Macabre or something like that. It’s called a tool for automating check of complexity. And yeah, we did add some rules there, but it’s usually not enough because those rules can usually be played out somehow.

You may use something else than if. I’ve seen a lot of tricks to just comply with the required metrics, such as code coverage or function complexity, and then maybe one thing is abstracted away to another function, but previously big function still calls now abstracted one, so the dependency is still there, is still impossible to test. So yeah, you may get to read of some if statements, but there is still a problem of testability.

The Problem of Testability Darko Fabijan (11:34):
And better to fix the culture.

Jan Giacomelli (11:36):
Yeah, but it’s way harder to fix the culture. It’s easy to just set the max complexity is 10 or 8 or whatever, compared to actually change the culture of your fellow engineers in a team.

So that’s one anti-pattern and I’ve seen a lot. And then another thing that I’ve seen a lot is direct coupling to database. So database is almighty thing which knows everything, and at some point, maybe even do everything because why don’t you just write store procedures and SQL functions, and then you don’t need to write Python at all.

In very simple cases such as, for example, content management systems, which for example, Django was designed for. There are usually no complex business rule or complex collations of anything. And you can leave there by almost omitting unit test.

You can actually just test your endpoint because your endpoints are directly coupled through the views to your models, which are using active records. So that means one instance is one row in database.

Jan Giacomelli (12:42):
So in such cases, there is direct uplink, but you can live with it because there is nothing complex to do. For example, I’ve seen a lot of direct uplink to database when dealing with machine learning problems. People have a lot of data and they need to do something with machine learning models and they write a class or a function which ties together, let’s say, NLTK model with the database access layer.

And when you try to test it in any way, you would rather kill yourself than to do testing. So you just push it and hope for the best, but even in the deterministic problems where you can exactly predict the output, it’s very unlikely that this approach will produce any good results. But when you add all the machine learning problems from model drifts and data changes throughout the time, because people change, the world is changing and everything. It’s just a pain in the ass to do anything with it.

Jan Giacomelli (13:42):
And you don’t know to which part the business logic is going, to which part the NLTK models are going, and to which part the database access is going. You just have one big mess, which should produce some sort of, let’s say, is this review positive or negative? But if you need a database, just train your model. That can be a little bit annoying.

I think that very good thing in Python that it tries to be as simple as possible. So when using, for example, pytest, you can reduce your boiler plate codes to a minimum comparing to unit test. You can also do all sort of stupid things with fixtures, but you can do stupid things with basically anything in this world, so you can abuse any tool. If you have a hammer, everything looks like a nail.

Jan Giacomelli (14:34):
I think that the good thing in Python is that definitely that writing a test can be a very, very, very simple task to do, and also it can be very, very simple to learn how to do it because the boiler plate is really at minimum when using pytest. But on the other hand, because it’s simple, it can survive 1,000 lines of spaghetti and it’s working today.

In Python, writing a test can be a very, very, very simple task to do, and also it can be very, very simple to learn how to do it because the boiler plate is really at minimum when using pytest. But on the other hand, because it’s simple, it can survive 1,000 lines of spaghetti and it’s working today.

-Jan Giacomelli

Unit Tests Darko Fabijan (15:41):
You touched upon a very concrete testing framework that comes built in into Python as a starting in library. Can you tell us a bit more about yours experience with unit tests? You also mentioned in our prep call just the definitions of what unit tests are and how different people understand it differently. And also then other types of tests that come into play at some point, integration, acceptance, and pen tests.

Jan Giacomelli (16:09):
So when I was starting, I really struggled with testing pyramid and what the unit tests are or what they are not, and what integration tests are and stuff like that. But at the end, my first and the most important rule for any testing, whether it’s a part of test-driven development, or it’s me writing end to end test for API, doesn’t matter, the first rule is if this tests fails, does it mean that this code shouldn’t go to production and vice versa? If it passes, can I confidently ship that to production?

The first rule is if this tests fails, does it mean that this code shouldn’t go to production and vice versa? If it passes, can I confidently ship that to production?

-Jan Giacomelli

If I cannot do that, if this test is flaky or if it does not give me confidence, then it’s useless, then it’s better to just remove it because I won’t take it into account at some point, because if everything is red, you stop seeing that it’s red. Or if everything is green all the time, then what’s the point? I can remove that and everything will still be green.

Jan Giacomelli (17:09):
So that was my first rule of thumb, and after these day, I still use that as a first measure for any test that I write. So if these stats is here just to satisfy coverage percents, or just to make my shape of test environment, instead of something else, then it doesn’t make sense to have it there. Butter just get rid of it, because it won’t do any good. I need to maintain it. I will have to check if it fails for some unknown reason, and it should pass at this point.

This is the most important thing that I would say is when writing any test. And then when you have this confidence, you start thinking. So when you have one test and you just need to make sure that your method stores your object as a rolling database, that’s fine.

Jan Giacomelli (18:05):
But when you have 1,000 of such tests, then there is the question of speed. So if I need to wait couple of minutes just to run my test, excluding end to end test, I won’t do it. If it takes couple of seconds, I’ll do it regularly because I want to see and I want to know whether I did what I think I have done, so if I made requirements, as soon as possible, because why I would wait if I don’t need to? So then in this matter, then, you need to start to think about… I can use, for example, test doubles to get rid of database access, to run all my unit tests and what can I do? I can implement in memory repository instead of a PostgreSQL repository, which must pass my contra test for PostgreSQL, but it’ll just store my object in memory in list or whatever. It doesn’t really matter.

Jan Giacomelli (19:10):
And now my test can go from couple of minutes to couple of seconds. And if I need to wait five seconds for 1,000 tests to run. I will run them 10,000 times per day because it just feels good to have everything green. And in this matter, clean architecture helped me a lot.

So how to draw the boundaries between different parts of the system, how to separate the data access layer from my business rules, how to separate the main business rules, such as that my user cannot have empty username for example, or that it must be a valid email address or anything else that I just want to make sure that ensures the integrity of my model or object. It helped me really a lot to start seeing those boundaries. And when those boundaries are there, you see that your system is testable because it’s very easy.

Jan Giacomelli (20:08):
For example, it’s very easy to inject in memory repository if it has the same interface to your use case, comparing to some mocking with patch, for example, in Python. So you can say that in this module, I want to mock my PostgreSQL repository and do something with mock, but mock will tell me what I want to hear, not what I need to hear.

So that’s why it’s better to just implement a simple test double which passes a contra-test and use it for all the tests. And my test will be fast and there will still be the same test. If I was able to sleep good with them when using PostgreSQL repository, I will be able to sleep then when using in memory repository. And I can just use integration test for example, to verify that my PostgreSQL repository is communicating with database as expected and that everything is there as it should be. And that’s it.

So our test suite was all good and then it exploded… Darko Fabijan (21:03):
One question connected to this, but also to something that you mentioned at the very beginning, that you came to projects that were like maybe 45 minute area to get the feedback, and then you went to 8 minutes or something like that. And through this, what you have been talking about, this is another tool to also improve that feedback loop.

Do you have experiences where it was very good and then the test suite started exploding, so you just figure out, “Okay, three months before we had a feedback loop of 8 minutes and now suddenly it’s 12 minutes,” and it’s actually that maybe there are more people on the project just tests are getting slower just by the way that they are written and to make my question really concrete, do you have experience with doing iterations? So, “Okay guys, we need to now have an iteration to do some changing in the way that we write test on how we will optimize our test.”

So yeah, if you can shed any light on such potential experiences.

Jan Giacomelli (22:13):
Yeah, yeah. We’ve had such problems, but it wasn’t that linear. It was one day the tests were running in, let’s say, 3 minutes and the next day they were running for 15 because we added so many tests in order to make sure that everything was working. So yeah, we sat down and then we tried to eliminate all the tests that were not producing any value.

So for example, we had to check that data are extracted correctly from bunch of invoices, but we were able to find examples which were green more or less all the time. So if anything changed in a code, they weren’t really failing. So yeah, we tried to change our codes to do some stupid things, and then we saw, “Okay, we can get rid of these test and this one and this one,” and then yeah, we went from 15 minutes back to not 3, it was 4 or something like that, or maybe 5, but it was a huge difference.

But yeah, we had to experiment with and do some sort of triangulation to see where the redundancy is.

Darko Fabijan (23:27):
Yeah, that’s a great example. There are usually quite a few redundancies in test suite, but the thing is that it’s not always easy to discover them and then you want to stay safe and then you don’t make move.

Jan Giacomelli (23:40):
Yeah, we were lucky there that those tests were covering more or less the same part of a system, but we wanted to extend our safety net as much as possible. So that’s why it was maybe easier to find redundancies inside a test because we knew which part of the code discovered by then.

Flaky tests Darko Fabijan (24:01):
Clear, clear. And maybe one last question in this area, if you have any practical advice. 10 years ago and today again, we see a lot of people struggling with different kind of flakiness in their tests. And there are many different ways that people run into that. At some point, maybe in prep call, you mentioned you will use a race condition somewhere, and then if you just try it a good number of times, it will start showing itself.

So do you have any maybe practical advice in area of flakiness, detecting it, dealing with it, and maybe some war stories that you can share.

Jan Giacomelli (24:38):
Yeah, flaky tests are usually assigned that your code is not, let’s say, deterministic. So there is some part which can change under some conditions. Usually this is some dealing with dates and times. This can really quickly lead to flakiness or with some external systems such as elastic search clusters or stuff like that. At least I had the most issues there.

So usually I just try to draw on a blackboard on which cases the tests are failing. So for example, there was no real pattern, so then I just write down into our blackboard every time that the test failed and the time of it and pipeline ID. And then I think at one point we realized… Yeah, one test was failing if it was running around midnight, because if it started just before and after, so it was very rare occasions because we usually didn’t work work at this point the day.

Darko Fabijan (25:49):
That was my next question. Are you working at midnight?

The reasons behind flaky tests Jan Giacomelli (25:52):
Yeah, but sometimes if you did a lot of changes, you may push, let’s say 10, 15, 20 times from, let’s say, 11:00 till 2:00 in the morning, and you may encounter this issue because this test, for example, was not that fast. I don’t remember what was the reason, but it was a good one. So yeah, it was not that hard after all to hit this break, so it may take a minute or two. At some point I realized, “Yeah, okay, this is this problem.”

But there is no thumb rule I would have. Usually it’s just sit it down and take a look why is this happening? Does it look like something that you maybe could reproduce in some way? Do you rely on anything else than your code? Do you want to touch file system or database? I think that most tests that are flakey are usually assigned that there is some external dependency that is not there all the time.

Jan Giacomelli (26:54):
So this might be the order of tests. This might be a row in database. So for example, when writing end to end test, you should create user, log in with it, do your stuff, log out, delete user. If you rely on an existing user, someone will come, one developer, one day, and it’ll remove it then. Why our test is failing, what did you break? Oh, there is no user. That’s an example of dependency which is not stable. But if you create all your resources, even for end to end test and remove them afterwards, okay, only for the cases that there is actual defect and you won’t come to a point where you would remove, for example, this user, then it’s very likely that your test will be flakey at least at some occasions. And it’s not good if your tests are flaky because they make you to start having doubts about them and whether they’re useful and it’s tempting to say, “Eh, nevermind, end to end test failed. Yeah. We know that it’s failing sometimes.” Just move to production.

Darko Fabijan (28:05):
Yeah. From my experience and experienced other people I talked to, yes, it boils down to discipline, essentially documenting what is happening. And I spoke with quite a few people that, taking a spreadsheet, essentially, as you said, with the blackboard, just documenting all the cases, and over time a pattern will emerge and someone will come in on that day with enough energy under his belt and will deal with one of those and just letting it get out of control and then poison the test suite, because if there are like quite a few of them, then people can really lose the trust in the test suite, which is very unhealthy.

Jan Giacomelli (28:48):
Yeah, that start the start of decreased amount of test and the qualities start to decrease and everything just goes down to a point where it’s very hard to develop new features, your feedback loops are longer and longer, and you just wait each other to finish your work and to resolve merge requests and stuff like that.

Darko Fabijan (29:09):
Well great, Jan. I mean, I think people heard quite a few very good war stories and that are backed by a lot of experience in the domain. Thank you so much for sharing all this with us and good luck with your career.

Jan Giacomelli (29:26):
Yeah. Thanks.

The post Jan Giacomelli on the Benefits of TDD appeared first on Semaphore.

View Details

Think of a world where you don’t need a separate testing environment, where you can test everything in production and capture valuable data that helps you improve along the way. The secret ingredient: feature flags.

If you can’t decide if testing in production is a foolish or genius idea, this tutorial will definitely help.

What are feature flags? Features flags is a software engineering technique that lets developers integrate code constantly into the main trunk. It involves shipping incomplete features into production, which remain dormant until ready. Feature flags also take part in software delivery; when a feature is complete, the code can be activated with the flick of a switch.

Feature flags control which code paths are active at any given time. Also known as feature toggles, switchers, or flippers, these flags can be switched on and off — either at build time or at runtime — allowing teams to change the behavior of an application without having to update the code.

Benefits of feature flags Despite adding a layer of complexity in the codebase, feature flags are powerful when it comes to software delivery:

  • Short development cycle: without feature flags, you have to hold off deployment of a feature until it’s thoroughly tested — a process that can take weeks. With them, we can deploy several times per day, try partially-developed features, and get instant feedback.
  • Simplified version control: we can do away with long-lived topic branches. Feature flags encourage using trunk-based development. We can merge every day, integrate continuously, minimize merge conflicts, and iterate much more quickly.
  • Test in production: new features can initially be enabled only for developers and beta users. No separate testing environment is needed.
  • Decouple business and technical decisions: sometimes a feature is ready, but we’re quite not ready to publish it. Feature flags allow us to switch it on when it makes the most sense.
  • Fine-grained releases: feature flags permit a high level of control to conduct canary or blue-green releases.

Use cases for feature flags First and foremost, feature flags are used to release new features. In addition to canary launches, you can do cool things like activating features for a seasonal event (think Black Friday) and installing per-user or per-region toggles. No other technique offers such a degree of control.

The roadmap for using feature flags is:

  1. Code: deploy the new feature, which is initially disabled for everyone.
  2. Test: when the feature is complete-ish, toggle it on for internal testers and developers.
  3. Canary/Beta: after sufficient testing and enough iterations, toggle it on for beta users or a percentage of the general population.
  4. Iterate: collect metrics and usage analytics. Gather feedback. Continue iterating.
  5. Release: finally, toggle the feature on for everyone.

Deployment and release must be separate. You should decide when to make a feature available based on business parameters, not on technical merits. Feature flags are one way to achieve this.

Running experiments with feature flags Since feature flags allow us to change behavior with such a fine degree of control, it’s the go-to method for conducting experiments. We can use feature flags to compare alternative versions of a feature.

Say you want to add a Call to Action and you have two alternatives. One is a short form and a button. The other, a single big round button. Want to find out which one gets more clicks? Feature flags make it simple to set up A/B experiments.

So you run your test: the first half sees option A and the other half sees option B. After collecting usage data for a period, you’ll see which one is better. One last switch will toggle the winning option for all users.

Feature toggles as operational switches Not all toggles are temporary. Some of them can be permanent. Having a limited or “lite” version can be a life-saver in high-demand periods.

What’s more, a flag can act as a kill switch to disable code that is causing a crash. Thus, feature flags give the ops team a quick way of reacting to problems.

Flags as an alternative to branching Let’s say that you’ve got a great idea for a new feature. What’s your first instinct as a developer? To quickly pull the latest revision, create a new branch, and get to work. After about 30 minutes, you have a working prototype. All tests are passing and things are looking good.

Yet, you hesitate to integrate the branch into the main trunk because the feature is incomplete. There’s still work to do. So you keep the branch isolated for a few days. What could go wrong, right?

By not merging the change right away, you’ve missed a vital moment. When the feature is finally ready, the hidden cost of branching emerges — teammates have merged their code in the meantime, the trunk has moved on, and you now have the work of fixing the conflicts ahead of you. You will need to redo some of your work in the best-case scenario. At the very worst, you’ll have to discard some of your changes.

The moral of the story is that you can’t wait until a feature is complete to commit it to the main branch, because the longer a branch exists, the higher the chance of a conflict down the road. It’s like playing a game; the further you advance without hitting a checkpoint, the more time you’ll waste when you invevitably die and have to reload.

“If you merge every day, suddenly you never get to the point where you have huge merge conflicts that are hard to resolve.” — Linus Torvalds

Hopefully by now, it’s clear why long-lived branches are bad. We must shrink them to the absolute bare minimum — if code is constantly being merged into the main trunk, there are little to no integration conflicts. Ideally, we should be merging about three or four times per hour. At the very least, once per day. That way, you know you’re safe.

For this to work, we must be comfortable with committing partial features into the main trunk. Here is where feature flags shine. Feature flags let us share new code to the team while preventing users from viewing/using incomplete features.

Implementing feature flags To visualize how feature flags work, let’s imagine we are building an e-commerce site. We have a recommendation engine that picks suggestions based on the products users are browsing.

We think we can make a better engine by using machine learning. The hypothesis is that getting better suggestions will result in more sales. But getting the model right takes time, so we’re not prepared to release it overnight. We want to have room for experimentation.

So, we deploy both versions of the engine:

``` // returns the active recommendation engine function engineFactory(){ let useML = false; //let useML = true; // UNCOMMENT TO ENABLE NEW ENGINE

if(!useML){ // SINGLE TOGGLE POINT
    return classicRecomendationEngine();
}
else{
    return MLRecomendationEngine();
}

}

let recommended_products = engineFactory()(viewed_product) ```

This pattern lets us have the toggle point in a single place. Uncoupling the decision point from the rest of the code is essential; otherwise, we’ll end up with if-then-else popping up all over the place.

Comments may work for quick experiments, but it’s cumbersome. Can we do better? We can have a router that knows the state of each feature flag and returns the correct engine. As long as the interface to the engine is standardized, they are interchangeable. This pattern is called branch by abstraction.

``` import toggleRouter from "toggleRouter"

// toggle router for engine function engineFactory(){ if(features.isFeatureEnabled("use-machine-learning-engine")) { // TOGGLE ROUTER return MLRecomendationEngine(); } else{ return classicRecomendationEngine(); } }

// instantiate the correct engine and use it let recommended_products = engineFactory()(request.viewed_product); ```

Features flags are not limited to true or false. We can have multivalue flags. For example, we can write a low-quality naive engine that uses fewer resources to handle traffic spikes.

The more code paths we have, the more tests we’ll need to write. A unit test for the recommendation engine should cover all the supported alternatives.

``` import toggleRouter from "toggleRouter"

describe("Test recommendation engines", function() { it("works with classic engine", engine => { const recommended_products = engineFactory('classic')(current_product); // check results });

it("works with ML engine", engine => {
    const recommended_products = engineFactory('machine-learning')(current_product);
    // check results
});

it("works with ML engine", engine => {
    const recommended_products = engineFactory('naive')(current_product);
    // check results
});

}); ```

Turning on feature flags Now that we have our first feature flags coded, it’s time to decide how to turn them on. The question boils down to two alternatives: at startup or during runtime.

Configuring flags at startup is the simplest and preferred solution unless you need dynamic toggling. You can store the status of all flags in a config file in the same repository as the project, and employ environment variables or command-line switches.

Runtime toggles Are static flags not flexible enough for you? If you need to level up the complexity, you can use a feature flag database. This lets you change settings on the fly, audit changes, and log feature utilization. You will, however, have to carefully manage flag status changes.

A somewhat less complex alternative is to control flags by request, that is, to activate a feature when a given user logs in, on a special day of the year, or when a request includes a special cookie or header. You can build the flag-selecting logic into the application.

Feature flags and CI/CD Let’s look at feature flags from the perspective of CI/CD. When you practice continuous integration, you have to decide how you will test feature flags. There are two ways to go about this:

  • Build stage flags
  • Runtime flags

In build stage flags, the state of all flags is known at build time. Since you define every flag at the beginning of the CI pipeline, you’re testing the same build that will be deployed.

The flip side is that we have to rebuild and redeploy the application for a feature flag change to take effect.

📙 Stacks such as Docker and Kubernetes let you rollout application updates without interruption. You can learn more about that in our free ebook: CI/CD for Docker and Kubernetes.

Runtime flags and CI/CD You must set flags either while deploying or at runtime if you don’t do it in the build stage. In both cases, you can’t know which features are enabled during the build, so your pipeline may not be testing the same artifact that ships to production.

And because you can’t test all possible toggle combinations, it’s imperative to have sane defaults (the application should run well without any flags defined) and be smart about testing. You must plan carefully anticipate which toggle permutations could clash and write adequate tests.

Recommendations for using feature flags * Use them with measure. Feature flags can get out of control. * Don’t ever, ever, ever repurpose a flag. Always create a new one for every change or risk making a half a billion-dollar mistake. * Minimize flag debt. Delete old and unused flags. Don’t let dead code linger. * Unless you have a good reason not to, keep the feature flag lifespan short (weeks). * Choose descriptive names for your flags. New-Feature-2 is NOT a good name. * Adopt trunk-based development and continuous delivery. * Consider having a way to view the state of all your flags. You can write an admin dashboard or an API for this. * Track and audit flag usage.

Final thoughts A feature flag adds to the system’s complexity but brings more flexibility. You can ship code more frequently, test on production, and wow your users by revealing a feature at the right moment. Mastering feature flags has basically become a requirement for trunk-based development and continuous delivery.

Further reading:

  • Automated Testing: The Cornerstone of CI/CD
  • The Benefits of Acceptance Testing
  • 20 Types of Tests Every Developer Should Know
  • [Podcast] Gleb Bahmutov on Why Testing Pyramid Makes Little Sense

The post Feature Flags: A Gentle Introduction appeared first on Semaphore.

View Details

As the end of the year is approaching, it’s a good tradition to look back at what the year’s been like. In these times of uncertainty, it’s reassuring that one thing remains certain – tuning in to some of your favorite podcasts and hearing familiar voices.

2021 has been an amazing year for Semaphore Uncut. This year, we released 26 episodes and welcomed engineering leaders from a wide range of backgrounds, from CEOs and company founders to framework creators and industry thought leaders.

While every episode of Semaphore Uncut is an insightful conversation on a whole bunch of topics, you, our listeners, favored some of the episodes more than others. We collected the top 10 episodes of 2021 that you listened to the most during this turbulent year. Cheers to 2021 and look forward to seeing you all next year!🥳

Unicorn Developers with Filip Hraček In this episode of Semaphore Uncut, we welcomed Filip Hráček, developer relations engineer at Google, and talked about what’s great about Flutter, how to out as a developer, and the importance of keeping your motivation up.

Filip shared some great thoughts about the art of finishing projects. Developers are great at tinkering and optimizing, and then it’s really hard to finish unless there’s some outside pressure. This comes especially true for side projects: one might just keep working on that side project forever and never finish. According to Filip (and we can only agree), it feels much better to finish and launch something, even if it’s a side project.

I think that’s important to realize that it feels much better if you actually launch and finish something, even if it’s a side project, but a lot of people will not do that because it means a lot of, kind of self-discipline and sometimes pain that you have to go through.

-Filip Hráček, former developer relations engineer at Google

Adam Gordon Bell on Better Builds with Earthly In this episode, we welcomed Adam Gordon Bell, Developer Advocate at Earthly, host of CoRecursive podcast. We talked about what Earthly is and how teams can adopt it. We discuss whether being a late adopter is a good or bad thing. Adam also shares his experience with starting his own podcast.

With Earthly, you can use whatever tools you’re currently using. We tried to give people the benefits of a big tool but that would be more approachable for everyday people.

-Adam Gordon Bell, Developer Advocate at Earthly

Adam talked a lot about why and when one should start using Earthly, how it works, and how easy or difficult it is to start with. Adam’s story as a developer is inspiring, and even more so is his podcast – CoRecursive. “There’s things to learn from people’s stories”, says Adam. “I like to focus on the experience, the experience of software development”.

Co-creator of Ionic Framework and StencilJS Adam Bradley on How to Make Fast Websites In this episode of Semaphore Uncut, we had an insightful conversation Adam Bradley, co-creator of Ionic Framework and Stencil, currently Director of Technology at Builder.io. We talked about Ionic and Stencil, Adam’s new projects Qwik and Partytown as well as how Builder.io works and how is it different from other drag and drop website builders.

Builder.io has that sweet spot where marketers and designers can go and build great landing pages, chnage around whatever they need, hit publish and get the page to go live immediately – but inside of their existing sites.

-Adam Bradley, Director of Technology at Builder.io

Adam speaks passionately about the new project that he’s working on – Builder.io. From the conversation, you’ll learn how the team managed to solve complex problems like making websites load fast with a new framework called Qwik and the Partytown library.

Lyft’s Keith Smiley on How to Scale Code with Bazel Keith Smiley is a Principal Engineer and Lead Maintainer of Bazel’s iOS support at Lyft. We sat down with Keith to talk about what Bazel is, how the team at Lyft adopted it and when and how to get into Bazel in the first place.

If your team is thinking of switching to a monorepo or adopting Bazel as a technology, this insightful conversation with Keith will help you to understand how to navigate this project.

The benefit of using Bazel is that you don’t have to use exactly what Bazel provides out of the box. You can abstract it so that the interface that your developers use is more specific to you

-Keith Smiley, Principal Engineer at Lyft

Brandon Bayer on Creating Blitz.js, the Fullstack Framework for React In this podcast episode, we welcomed Brandon Bayer, creator of Blitz.js, CEO of Flightcontrol. Brandon talked about his project Blitz.js, a Next.js fork and a fullstack, batteries-included framework for React. We spoke about the inspiration for the project, the challenges along the way and what’s planned for this great framework’s future.

Gleb Bahmutov on Why Testing Pyramid Makes Little Sense Testing pyramid is a concept that’s familiar to many developers and QA engineers. But what if we told you it makes little sense?

In this podcast episode, Gleb Bahmutov, Senior Director of Engineering at Mercari US, explains why we as engineers should rethink our approach to writing tests and what we could use instead of the traditional testing pyramid.

People write so many unit tests and so few end-to-end tests because by the time they get to end-to-end tests, they just run out of time.

-Gleb Bahmutov, Senior Director of Engineering at Mercari US

Mapping the World and Testing Against Reality with Eric Bowman In this podcast episode, we welcomed Eric Bowman, CTO at TomTom, former VP Engineering at Zalando, and a developer behind the game “The Sims”. We discussed how mapping technology has changed the world and how CI/CD practices have made real-time mapping possible.

Eric talks about his work at TomTom, how technology helps our daily lives, and the importance of feedback.

People so often they think of value flowing one direction, but the reality is that to do anything interesting value has to flow both directions, kind of all the time. Anyplace in technology now where you’re not getting feedback, it’s a missed opportunity.

-Eric Bowman, CTO at TomTom

Multi-platform Development with Co-creator of Flutter, Eric Seidel In this Semaphore Uncut episode, we meet Eric Seidel, co-creator of Flutter and Director of Engineering for the Flutter team at Google. We chat about the Flutter developer experience, the Dart language that powers it, and the transition Flutter is making from mobile to desktop.

Dart is fantastically easy to learn. It’s an intentionally small language. It has few pieces in it to try and make it easy to learn, easy to use.

-Eric Seidel, co-creator of Flutter

Dave Thomas on Testing Past, Present and Future In this episode of Semaphore Uncut, we talk with Dave Thomas, author of The Pragmatic Programmer, and many other well-known software engineering books. We discuss how software engineering has changed over many decades and how Dave’s experiences have informed his attitudes to testing.

You’ll learn how the Internet of Things is transforming software architecture, why software is both abstract and yet it changes the world, and why we should treat software testing as a tool, not a religion.

Nader Dabit on How Managed Services Turbo-Charge Startups In this episode of Semaphore Uncut, we meet Nader Dabit, Senior Developer Advocate at AWS. He gives us his perspective on managed services in cloud development, tells us about AWS Amplify, and gives his predictions on the future of cloud computing.

Among other things, you’ll learn about the 80-20 rule of cloud apps, how Infrastructure as Code allows incremental design of your production environment, and Nader’s future in decentralized finance.

I think the core bread and butter for almost every app is authentication, data, and storage. So, you need authentication for user management. You need a data layer, which is essentially a combination of an API and a database. And then you need some type of file storage, typically for images and videos.

-Nader Dabit, Senior Developer Advocate at AWS

On Our Way to 2022 We have great plans for Semaphore Uncut in the upcoming year, bringing your way conversations with industry leaders, book authors, and international speakers. Is there anyone who you’d like us to invite to the podcast? Let us know!

Happy listening and see you in 2022.

The post Top 10 Semaphore Uncut Episodes in 2021 appeared first on Semaphore.

View Details

Mobile apps today aren’t just about retrieving & sending data to and from services. They are results of a combination of sophisticated state management, aesthetically pleasing animations, complex navigation flows, processing of huge data sets, inference of ML models, and much more.

Additionally, multiple developers spread across different teams are needed to deliver and maintain these features. We have seen a higher adoption rate of React native amongst app developers recently. The great vision by the RN core team at Facebook makes it a top choice for mobile app developers.

Using React native architecture and writing scalable React Native apps requires a good understanding of practices that can keep up as the size of your application grows. In this article, we will discuss a few tips to architect highly-scalable React Native apps.

Enabling solid constraints To ensure that apps remain performant and scalable as the scope grows, teams have to be very vigilant about the architectural decisions they make. When it comes to better architecture for react native apps, one thing that makes a huge difference is having a good answer to the question of: “What happens when a change is happening in one module?” To ensure that your app doesn’t turn into a big ball of mud, a set of practices and constraints must be established at the very beginning of development.

Dividing apps into smaller, independent modules that are testable in isolation can be super helpful in maintaining development pace & quality over time. It also allows teams to work independently, resulting in a well-aligned, fast development process with seamless deployment and incremental upgrades.

Key takeaways:

  • Best practices for large-scale React apps
  • Monorepos
  • Micro frontends

React Native Architecture – State management State management is one of the most debated topics in today’s JS world, and we are seeing a lot of state management solutions pop up. It is a crucial part of apps, especially for medium- to large-scale applications.

Deciding which is the best amongst all of these solutions largely depends upon the requirements and team’s expertise. Important factors in selection can be developer experience, an ecosystem built around a library, or community adoption and support.

Redux has been in state management for a long time now and gained a lot of attention. Recent developments in Redux ToolKit & RTK Query have boosted its power to great extent. React Query is another awesome solution when your app is more API-centric. MobX, Recoil, and a few others are also making waves and might suit your requirements better.

Tools/Libraries:

  • Redux ToolKit / RTK Query
  • React Query

Code sharing The best part about React is its ability to port concepts and ideas from one platform to another. The motto “learn once, write everywhere” is more practical than ever before. If the foundations of React Native apps are laid down properly, it can enable teams to share a huge code base across different platforms including Web & VR. Teams can also leverage git submodules to share components and logic across different repos.

Libraries like expo and react native web are paving the way to porting 100% of the code base across web and mobile platforms. For small- and medium-scale apps this can be an ideal way to save money.

Tools/Libraries:

  • Git submodules
  • Expo
  • React Native Web

Theming It’s rightly said that great design can lead to the best front-end architectures. While applying sophisticated techniques like monorepos & micro-frontends, a design language can serve as a cornerstone for solid architecture.

A good design system sets the tone for your design and development teams. Consistency across components used in different parts of the app allows developers to create better APIs for each component. This makes components more usable and avoids incurring extra technical debt over time.

Wrapping dependencies Large-scale React Native apps usually depend on a lot of third-party libraries that help them work faster. As time passes, however, a situation might arise where a third-party library doesn’t serve its purpose as well as it did when we started using it or a better alternative might emerge.

In such cases, switching from one library to another can prove to be a costly decision. As this change can hit our app quite hard if the library in question is used widely across different modules. It is always good to wrap your own API around any such libraries and use them via wrapper across your app. This can be super helpful in the event that you switch third-party libraries.

Moreover, be wary of the fact that too much abstraction can make your code uglier, so be very careful and wise when introducing any such abstractions. Additionally, platform portability can be ensured if platform-dependent libraries are wrapped with custom APIs.

Maintaining code & repo quality As an application grows in size, the execution of tests becomes more costly. Thus, it is best to plan ahead to save time, and make sure there aren’t any fundamental issues (such as typos) in our code.

Static typing & linting can help us avoid basic issues that could eat up tons of our CI resources. Issues like spelling mistakes, linting, static type checking, and package audits can be performed on a developer’s machine as soon as they commit any code to the repository. Git hooks can also assist in maintaining code quality across a large codebase with several contributors.

You can also use git hooks to check commit messages, make commit history more readable, and for generating automated changelogs.

Key takeaways:

  • Lefthook for React Native CI

Tools/Libraries:

  • Commitlint
  • Lefthook
  • CSpell
  • Linting

Testing “Move fast & break things” is no longer an acceptable mantra in today’s world. Benchmarks and expectations are getting higher with every passing day. For apps being developed at a high pace, automated testing is the most crucial step for continued success.

An industry expert, who has been working as a software developer for the past 30 years, recently mentioned that testing is still the most difficult part of software development.

Thankfully, we have tools that can make testing a joyful experience.

For developers looking to start with testing in React Native, we have put together an in-depth guide to implement unit, integration, and e2e tests for React Native apps.

Key takeaways

  • Comprehensive guide for Testing React Native apps

Tools/Libraries

  • Jest – unit testing
  • React Native Testing Library – integration testing
  • Detox – e2e testing

CI/CD Today, moving fast is not just an advantage, it’s crucial for the survival of businesses. CI/CD gives you the ability to ship your apps with confidence and peace of mind. If done right, CI/CD can significantly increase the pace of development & distribution .

As discussed earlier, a top demotivating factor for many developers is sitting idle and waiting for processes to complete. It’s in the best interest of developers and businesses to automate tedious tasks so that developers can do innovative and productive work. Besides allowing developers to deliver apps and updates faster, CI/CD takes away the cost of managing & maintaining different environments on development machines. As mentioned in the McKinsey survey:

“Organizations with strong tools for continuous integration and delivery—are 65 percent more innovative”

If you want to leverage CI/CD for React Native apps, we’ve put together amazing guides for you to fully implement best in class CI/CD for React Native apps.

Key takeaways:

  • CI/CD for React Native apps Android
  • CI/CD for React Native Apps iOS.

Tools/Libraries:

  • Lefthook for React Native CI
  • Semaphore
  • Fastlane

Performance Tracking Like we mentioned earlier, moving fast is a necessity these days. It’s not always easy, and faster iterations can result in problems even when a solid test suite and CI/CD are in place. For tracking issues and monitoring the performance of your app as it is being used, you can leverage Firebase SDK for its performance monitoring & crashlytics.

Key takeaways:

  • Firebase performance monitoring/crashlytics
  • LogRocket crash monitoring

OTA updates One of the top challenges faced by app developers is getting users to upgrade to new versions of apps. Unlike web apps, businesses have less control over what version of their app users are using. Both Android & iOS now have libraries and methods available to notify users whenever a newer version of the app is available. React Native In-App Update is an open-source library that wraps in-app updates for both android & iOS.

This might be useful for many apps but still isn’t enough by itself. Because most of the business & UI logic resides within the JavaScript in React Native apps, it is possible to update apps over the air. Microsoft CodePush enables us to update the JS bundle of React Native apps without bothering our users.

Key takeaways:

  • Microsoft CodePush
  • React Native In-App Update

Developer experience Developers are the core of every business that relies on technology. Delivering the best user experience is only possible when the people developing it have the best experience themselves. Developers equipped with the right development tools and processes are more committed and productive.

Automating repetitive tasks such as writing skeletal code, utility libraries, unit test cases, and performing testing, frees up a considerable amount of productive time for developers. Taking assistance from AI can also bolster the development speed of teams.

Key takeaways:

  • How developer experience impacts businesses – McKinsey
  • AI can bolster development speed

Conclusion “…with proper design, the features come cheaply. This approach is arduous, but continues to succeed.”

—Dennis Ritchie

As we write more and more code, our ability to foresee the future increases. We learn from mistakes and find ways to deliver more scalable products at a faster pace. Laying down solid foundations at the very beginning of development can ensure the reliability and quality of your code base as it scales. Any mistake in this phase can result in huge technical debt in the future.

Keeping the development experience of apps pleasant will result in better productivity. Enabling solid constraints in apps will help developers make fewer mistakes, and automation can boost your team’s confidence to ship features fast without worrying about things breaking. All of this results in business growth and better experiences delivered to end users.

The post Architecting Scalable React Native Apps appeared first on Semaphore.

View Details

In this podcast episode, I welcome David Burns, Head of OSPO at BrowserStack, Chair person for the W3C Browser Testing and Tools Working Group, core contributor on the Selenium Open Source Project. We talk about the past, present and future of browser testing, how to eradicate flaky tests and why it’s important to invest more effort into testing pyramid from the very beginning.

Key points:

  • David Burns on browser testing
  • WebDriver BiDi specification
  • How flaky tests are born
  • How to start new or migrate existing projects
  • Testing pyramid and how tools support it (or do they?)
  • Going BiDirectional with testing

Listen to the full conversation or read the edited transcript.

You can also get Semaphore Uncut on Apple Podcasts, Spotify, Google Podcasts, Stitcher, and more.

Like this episode? Be sure to leave a ⭐️⭐️⭐️⭐️⭐️ review on the podcast player of your choice and share it with your friends.

Edited Transcript Darko Fabijan (00:02):
Hello, and welcome to Semaphore Uncut a podcast for developers about building great products. Today, I’m excited to welcome David Burns. David, thank you so much for joining us.

David Burns (00:11):
Thank you very much for having me.

Darko Fabijan (00:14):
Great. Can you please just go ahead and introduce yourself?

David Burns (00:17):
So I’m David Burns. I am a Head of open source at BrowserStack and what we look after core open source projects that are important to BrowserStack, and make sure that they are sufficiently funded by other people or with money depending on kind what the project is. We’re a small engineering group at the moment, looking to grow bigger. Our core projects are Selenium, Appium and Nightwatch.

In the past, spent nine and a half years working at Mozilla as engineer and then engineering manager. So I’ve spent a long time in the open source space. I’m also involved in standards bodies. So I’m the chair of the W3C’s working group for browser testing and tools. So anything that we want to standardize to work across the industry will probably come through our group at some points and we’ll see where and how we can make it standard, which makes it easier for all users who want to do testing.

Darko Fabijan (01:23):
Thanks for this introduction. And I’m sure that a lot of our listeners would love to hear about the space that you have spent a lot of time in as browser testing and web is definitely a dominant platform. A lot of people are on a daily basis writing tests that involve browser, also debugging them. So I was hoping you could give us a tour of this space that a lot of us are touching in one way or another.

Darko Fabijan (01:53):
But I think that most of us dive in just as much as we need to get components in place. Is it a Chrome driver? Selenium driver what’s this, keep going. But we do end up spending a lot of time on bugging. I think in most cases it’s our fault, but we also tend to blame it on technologies that we are using. So please, if you can elaborate on this topic.

On browser testing David Burns (02:21):
So browser testing has been going for, oh, I don’t know, must be… I think Selenium turned 17 or 18 the other week, and it’s been important through the browser walls. So it got started there by Jason Huggins while he was a ThoughtWorks working on internal tooling and then another ThoughtWorks engineer created WebDriver at Simon Stewart and he took a different approach to how you should control a browser.

David Burns (02:50):
So Selenium would be controlling it from the outside in, which injects JavaScript into the page to manipulate events, as you need them. Where WebDriver was doing it slightly harder, but in a better way. So it would inject events behind the scenes.

So not actually in the page, which meant you had to have deeper integration to the browser. For certain browsers, that was really simple. Firefox, you just built an extension, you speak to the extension, which already has the internals, and with the Internet Explorer, you had to do weird and wonderful Calm interfaces, which if you’ve ever done Calm you know it’s painful, but it works and you always know roughly what you’re going to get.

David Burns (03:31):
And so we built up from there and then over the years, we just carried on improving the interfaces and things like that, until one day, one of the directors of engineering at Opera approached the project and said, you should make this a standard, we can see where you’re going. We think it’s important and you should make it a standard.

David Burns (03:55):
And so Simon came back to the project and I was working as Mozilla let the time and we had a chat and we said, yeah totally we should make this a standard. It makes perfect sense. And that was in around 2011. And so we built up the standard to what it is now, what browser vendors have all implemented. There’s a new standard coming out slowly but surely because what we did then and where the market is going is changing.

WebDriver BiDi specification David Burns (04:25):
And so we need to update that, which leads to the WebDriver BiDi specification. So Bidi means bidirectional, in the past, if you wanted to use Selenium, you would have to pull the browser. Is this thing ready? Which can lead to flakiness.

David Burns (04:43):
And it’s partly because people don’t fully understand how that side of things can work. And as you said, people only deep dive as far as they need to go. And sometimes you need that little bit deeper to understand where the problems are going to become later. And so that’s allowing us to have new APIs that are similar to newer products that are hitting the market and improve how people do their testing. And I think that’s the key part here is Selenium willing to move with the times and improve it.

Sometimes you need to dive a little bit deeper to understand where the problems are going to become later.

-David Burns, Head of OSPO at BrowserStack

How flaky tests are born David Burns (05:17):
One of the key things that I think touching on the subject of moving with times, originally Selenium was built to be this underlying API. So I think the best way to describe it is, it was supposed to be the database connections to your database and then someone would build your ORM on top of it.

David Burns (05:38):
That’s how it was originally designed, how we wanted it to do it. Fortunately, unfortunately, it depends on who you speak to. Some people like that, that you could go, I’m just going to do this. And then they could build their own world on top of it. And we saw the Googlers doing weird and wonderful things when I was at Mozilla. We did amazing things with Selenium, from doing basic performance testing, because we could control the whole environment where possible.

David Burns (06:10):
I’ve seen people doing games testing because they could control it and to the finite detail. And that was really, really cool. But as you mentioned earlier, I like that phrase, where people only dive down as far as they need to go rather than all the way, is that tech people now have to deliver features as quickly as possible and as quickly as possible tends to mean yesterday, not tomorrow.

David Burns (06:37):
And I think that mentality, especially being pushed from product managers and project managers, doesn’t allow people to understand where things are going. And so they’ll reach the zenith of their flakiness. Browser are asynchronous by nature to improve the performance and battery management and things like that. Because they can pause and do what they need to in different threads.

The mentality of shipping as quickly as possible doesn’t allow people to understand where things are going. And so they’ll reach the zenith of their flakiness.

-David Burns, Head of OSPO at BrowserStack

David Burns (07:05):
But people’s thinking is not asynchronous, especially when it comes to testing, they will go, I will want to do step one, step two, step three. And these two worlds really struggle to meld because they hit these points and then people are like, oh, but I’m not expecting the browsers to do that. Why is it doing that? And then obviously it’s Selenium’s fault or Puppeteer’s fault or Cypress‘. It’s every single one of those tools has the exact same problems because they’re all using browsers and it’s just not gelling with the mental models that people have. So it’s been a nice history and where the market is going, I think is quite cool.

How to start new or migrate existing projects Darko Fabijan (08:25):
Maybe from the very practical standpoint, long time ago, it seemed, and there was a default choice and people just went with Selenium and then you don’t have that paradox of choice. And some products are just starting out and people are deciding, okay, what’s the tag that I should pick and run. Or some people are actually migrating from one to the other and so, what would you recommend?

David Burns (08:55):
I’ll start with new projects. I think the problem that I see a lot in the space at the moment is people have forgotten the basics when it comes to how you set up new projects and how you go about testing it.

And so people will knock up their MVP super quick, get it ready. Go, yeah it was in production, then they’ll throw some tests at it and then cry and cry some more because their new project’s not working when they try testing, it’s not been designed for testing was not being designed for modularity, which testing enforces. And so we’ve got to the stage where I think the testing pyramid seems to have been thrown out the window.

Browser testing: the testing pyramid and how tools support it (or do they?) David Burns (09:48):
So the testing pyramid, this concept for listeners who might not know, it is this concept where you design your test in a large set for unit tests, a medium set for your integration tests and then a very small set for UI or tools like that, where you’re going from end-to-end.

David Burns (10:10):
And you want to minimize those tests because as you go up to the testing pyramid, you potentially increase the size of the flakiness. And I think that, because people have seem to have thrown that out. Some of the other tooling out there compared to Selenium is gaining a lot of traction because people struggle with that initial of core concept that they then go, well I’ve got this black box and this black box is solving this problem for me really well.

David Burns (10:43):
And then they try scale it out and anyone who’s done any scaling work will know that there is blood, sweat and tears behind every single bit of scaling from anyone who’s touched Kubernetes recently, they will tell you all the problems with it and they don’t necessarily know how to solve it, but everyone knows it.

David Burns (11:08):
And it’s because people that go, oh this is this cool thing. It’s nice for my CV. I can press this one button and it works brilliantly on my proof of concept, but it doesn’t necessarily work in the real world. And so when we look at the markets out there, there are a lot of tools that say that they’ve solved everything for you and they make big promises. I’m going to name some names and we’re not saying they’re better than Selenium or worse than Selenium.

David Burns (11:38):
I think they have their place definitely in the market. So Cypress is a nice black box is the way I look at Cypress when I compare them to Selenium and they go here is your gateway drug, I think is the best way… this will get you started. It has… you want to do API testing, we can do it. If you want to do browser testing, we can do it.

David Burns (12:02):
And then it’s, oh you want to now run these in all in parallel. Yeah, you’re going to have to pay for that part, but you know, you can totally do that and that’s fine. And it gives you this belief that they’ve solved the Selenium flakiness, and then you leave it for a while. And this Cypress then releases new APIs around waiting. Like I’ve got to wait for this new object to appear on the page.

David Burns (12:30):
They’re learning from the Selenium lessons in the past, just had a wake library, I don’t know, for at least nine years, somewhere in that region. And the same story came from Puppeteer. Puppeteer is very good. They’re initially Chrome-based, Firefox supported being added. And when I was a Mozilla I was leading the team that was adding it before moving away.

David Burns (12:52):
And again, there’s this belief that they’re solving the problem of waiting and flakiness and then suddenly, oh, this real problem still exists. And they add in APIs to add it. Those are really cool tools depending on your use case. And I think use case is where I don’t think a lot of people are putting a lot of effort into the way they think about the tools. Or I come back to Kubernetes, everyone’s talking about Kubernetes, can I release it quickly into some cloud provider and just press a button?

I think use case is where I don’t think a lot of people are putting a lot of effort into the way they think about the tools.

-David Burns, Head of OSPO at BrowserStack

David Burns (13:29):
And it’s, yeah you can, but then is it going to scale? Is it going to have this? Is it going to still be secure. Core problems that I think people easily forget about because they try to put certain amounts of developer experience in the tooling above all else. And I think that’s not a good thing.

Why it’s important to invest more effort into testing pyramid from the very beginning Darko Fabijan (13:49):
Something that I can contribute here. How things are looking at the ground, I’m speak about, let’s say most successful customers that we have. Goes like this, you put the absolute minimum effort in the beginning, of course, you are not too involved with your testing pyramid.

And the distribution of tests and so on because you’re just getting started and amount of money that you are dealing with and number of developers and all that. And he just said, can this be done for yesterday? And then it’s a pattern, comes a series, investment series, where they get something worth of 50 million and then development teams jumps from 15 to 20 engineers to 100 and 150 in less than two years… year, year and a half.

Darko Fabijan (14:46):
Features need to continue flowing out. Yes, test suit goes crazy because just a lot of people building web apps initially is very cheap. Okay, so I’m going to build my unit test like this into this layer. And then I’m going to do this through the integration test. And then as you said, just this top part I’m going to cover if something goes from hard drive all the way to clicking the browser.

Darko Fabijan (15:17):
That clicking the browser gets, as you presented, essentially abused in a way, used so much. On the other hand, I can say is good for our business. People have to parallelize their test suite used across 50 or 100 jobs in parallel. In order to get the feedback loop. You’re not quick enough so they can survive.

Darko Fabijan (15:37):
And usually a part of conversation is, okay a step that needs to be put in place because if you guys keep growing your test use in this way, with this very expensive, also very flaky tests, hard to scale, is not going to run. So essentially I guess I repeated everything that you said, but I mean, we really have a lot of samples like that. The more successful the companies are, the bigger problems they have in terms of testing.

The more successful the companies are, the bigger problems they have in terms of testing.

-Darko Fabijan, Co-founder and CTO at Semaphore

David Burns (16:08):
Oh, definitely. I’ve seen one case I’ve seen recently, and I’ve seen this with a competitor. So one of the nice things about working in an open source program office is that if a competitor has one, I get to speak to my competitors regularly about things.

David Burns (16:25):
And they’re very open because open source people are very open. Obviously, no names are ever mentioned, but I’ve heard this from internally and I’ve heard this from competitors as we’re dropping Selenium, we’re migrating to something else, six months later, we’re dropping that thing and we’re moving half the test suite back to Selenium. And this is the thing of where I say the testing pyramid seems to have been thrown out. And yes, I’ve worked… when I was at Mozilla I joined before B2G the Boot to Gecko project, which became the Firefox OS project.

David Burns (17:02):
And Mozilla grew so fast. It was my first time ever seeing a company grow so fast ever. When I joined BrowserStack, we were at the beginning of that, shooting up. Yeah it’s so easy to have problems of your code has scaled out and you are having these problems. It’s like, oh I’ll just move to this other tool. And it’ll become my nirvana and it rarely does.

I’ve moved around in the developer experience space, I spent all my time in developer experience, it was around tooling. So we made geckodriver and we made that really awesome and simple to use.

David Burns (17:44):
And you didn’t have to worry about upgrading. So earlier you were like, oh what version do I have to use for this or that? Or the next thing, one of the key things that I wanted for Geckodriver is upgrade when you need to not because you have to, and then you see the Chromium based browsers. It is like, Chrome driver needs to be upgraded every version. And I’m sure if you are putting this into your CI system, that’s every four to six weeks nowadays. It’s a non-trivial amount of work.

Darko Fabijan (18:11):
And it breaks things.

David Burns (18:13):
Yeah. People just think you just upgrade one line and that’s it. Not always, because now you’ve got to deal with all the fallout of any potential bugs that it’s introduced, right?

David Burns (18:23):
And it’s a lot of work. It’s hard. And I think caring about the speed as you said, right? Yes, it’s good for you when tools are abused because as your company, you can sell more. Your solution is scalable so you can sell that better. But that also then potentially costs seeds. And so people go, oh is this really worth it? And then it’s like, oh it must be the tool space. And that’s what you were saying over there. It’s not necessarily. Past me has written really bad code. And past me is really bad at coding. And I go back and fix all them and make it better and faster.

David Burns (19:01):
And you know, and that’s where I think a lot of the tooling is. And that’s where me on the Selenium project is trying to take this Selenium project is try help people fix those problems by being aware of it. But then as we more move into similar APIs as like Playwright or Cypress or Puppeteer, there are other problems that we’re running into, because we are hitting these scalability problems again, because instead of being this HTTP socket that we’re constantly speaking to, it’s web socket, but the web socket can speak back to you. And if you’re not careful, suddenly you’re doing a denial of service onto your test servers by accident and that’s going to be the next foot down. It’s like, oh my test is not flaky anymore. But my server keeps dying. Like why? You know what I mean?

Going BiDirectional with testing Darko Fabijan (20:33):
Going from here, let’s speak maybe more about the future. You mentioned about generally standard implementation of a driver or let’s say specification in the API. And you mentioned at that point, something about being bidirectional and so on. Can you give us maybe a tour of that that might be useful for people?

David Burns (20:53):
So I alluded to it so far in my discussion, but WebDriver BiDi is the term we are calling it and it’s about having Selenium be able to speak to a browser and the browser speak back to it. So one of the key things that a lot of people wanted, and we seen this through Puppeteer and we’re seeing it with Playwright, Cypress kind of, but Cypress architecturally can’t do all of it, is that they want to know when certain things happen in the browser and then carry on.

David Burns (21:23):
And so we are now creating originally WebDriver was HTTP because it was simple and standard and it scales massively, right? That’s how, BrowserStack for example, is able to scale. We just need to be able to process HTTP requests when we get them and process them properly and fastly and correct. And which is awesome.

David Burns (21:45):
Moving to the bidirectional way is that, so it can speak back to you, which is great for if you want to know while your test is running, is there a JavaScript exception suddenly thrown? How can I handle that? Or you want to know when a console message has come out. So you add a certain console message to help with flakiness, for example, you could handle that.

David Burns (22:07):
So we’re adding these new APIs around console logging, JavaScript exceptions coming back from the browser rather than, actually was there a message for me? No, there hasn’t. I’ll wait a little bit and then try again. You don’t have to wait because you know it’s going to come towards you. So there are features like that. We are going to hopefully be taking the whole WebDriver project to sit on top of these new APIs.

David Burns (22:32):
So when people use WebDriver, it’s still going to look the same. It’s still going to act the same. However, how it does it underneath is potentially going to change. And it opens up a whole new world of new features that we want and we can make things better, but sometimes moving it in that direction has its own problems.

David Burns (22:52):
It’s all about balancing the books and if you ask for console messages and you’ve put your debug version into production by accident, suddenly you’re going to get a thousand console messages a second potentially, because you are just admitting different, into this function, into this function, you know what I mean? And so you can get really hairy very quickly.

Darko Fabijan (23:17):
Yeah. Clear. You spent a lot of time at Mozilla and if you can potentially give us your view on the current state of browsers and alignment and all that. What’s your insider view on browsers?

David Burns (23:51):
I’m only still in the browser world through standards nowadays. Everything I say I’m caveating with that. So it’s not knife edge, but I think Safari is getting some unfair press at the moment. And I really think this, one of my last groups that I managed at Mozilla was the interoperability team. And one of our key tasks was managing the W3 test suite internally.

David Burns (24:21):
So we would make a copy of the W3C’s test suite. Well, it’s not W3Cs anymore, it’s called web platform tests, and we would run those against Firefox and then try process them. And we would upstream whenever there was an internal change and downstream them when in external changes. So we always wanted to keep that test suite up-to-date and we were doing pretty well. I haven’t looked at the numbers, but we were doing pretty well back then.

David Burns (24:47):
I think Firefox and Chrome were only 1% difference. And that 1% is not APIs that a lot of people would care about. It was fine. Safari was lagging behind, but why I think there’s a bit of unfair pressure is that every time Google adds a new API, especially for like Project Fugu, which is a brand making mobile features in browsers. So web USB, things like that, for example.

David Burns (25:19):
Adding those in Safari might be lagging behind in them. And I think it’s okay because there are standards out there, but browsers can pick and choose what they want and they can make sure that it aligns with their markets, how they see their markets. I’ve always felt that the Safari team is under resourced, especially for the amounts of money and size. I think they could definitely hire a lot more engineers, but driving that I think is important.

David Burns (25:48):
And so Safari is focused on battery life and done fantastic. So if you’ve got a MacBook open up Safari, use that as your day-to-day browser and your battery life is amazing. You open up Chrome on the exact same machine and you might only get half to two thirds of the battery life. And it’s about the focus that’s different.

David Burns (26:13):
I don’t necessarily agree with all the Fugu APIs that are being added. Some of them are really good. Some of them are just like, why would you really need that? And the pretense behind it, I don’t always agree with, but that’s just opinions, but I think it’s unfair on the Apple engineers because they are doing really good things and they’re focusing on what’s good for Apple.

David Burns (26:33):
Firefox is focuses focusing on what it can do to stay relevant in the market. And so its engineering team got cut down quite considerably last year with layoffs. And so looking at that, they’re going to start falling behind and unlike Apple who are pre-installed on laptops, it’s going to be harder to keep up. But I still believe browser diversity is good. Everyone moving to Chrome I think is a bad thing. I use Firefox is my default browser, because I still believe in the mission of Firefox improving the web and the internet for everyone and going from there.

Darko Fabijan (27:11):
Now that you’ve shined a bit more lights on it from a different perspective, all the browsers coming from very different places and trying to solve different things. What are important for them. And yeah, seems that from the specifications’ perspective, for the majority of the use cases that we generally have, they are aligned well enough to serve everything.

Darko Fabijan (27:32):
Yeah. Maybe a last question for me, you mentioned that you are focusing on open source strategy of BrowserStack and there are a number of projects involved here. Developers are always searching for something new to know about or potentially join. Can you talk a bit more about that? What are the projects you’re focusing on? Where do you see the help is needed?

David Burns (27:56):
So all the projects I mentioned going forward always need help and we are eternally grateful to anyone who can contribute some time to open source. And I appreciate this is not a space that a lot of people can do or they don’t feel comfortable doing. Sometimes people get scared going into open source. All of these projects I mentioned are really awesome about helping you contributors, especially people who might want to learn coding while contributing.

David Burns (28:24):
So anyone can help out here. So the main one that I help out on is the Selenium project. Personally, I look after the Python and JavaScript, those are my core language areas and help out there as well as I’m part of the technical leadership. So Selenium always needs help and there’s always bugs. I think we just passed on GitHub 10,000 issues and pool requests at the end of last week.

David Burns (28:50):
So it’s still a vibrant community. And so people are still using, it still raising bugs, still submitting pool requests, which is awesome. The next one is Appium, which is kind of like Selenium, but for mobile applications and the community there is awesome. One of the nice things about the Selenium project and Appium is the people there genuinely want to help.

David Burns (29:12):
And I’ve seen in both communities people come in and go, I have no idea how to program, but I need to do this for my job. And people teach them basic programming and then help them through. And so I really love these projects because of how they focus on community wonderfully and go there. And so the Appium project could always do with people they’re working towards their version two, Selenium just released version four, they’re moving forward at a really good pace. And so if you have time, help out there.

David Burns (29:44):
The other one that we contribute to is a project called Nightwatch.js. We’ve seen a lot of browsers that customers using it and it’s built on top of Selenium. And so it’s a test runner and it has all the things that you need. And so we’ve got some people helping out there and that project is a really cool project. There’s some really nice people there again.

David Burns (30:06):
If you have some time help out there. github.com/nightwatchjs, come help there. I hear JavaScript is an important language for the web these days. So I’m sure one or two people might know it and if you can help and if you are a bit worried about getting started in any of these projects, I’m going to do a slight plug, but it’s more to help open source projects is that, twice a week, I do a Twitch stream on getting people started in open source and getting them feeling comfortable.

David Burns (30:40):
And so I don’t plug any projects there. I don’t plug BrowserStack, it’s just about getting people started and come hang out code with me or ask questions and I’ll help you get started because I truly believe in open source. It’s part of my DNA nowadays, cause I’ve been in it for so long.

Darko Fabijan (30:59):
Great. Great. And I love hearing this Twitch and generally hold that new format is amazing. I now regretted that 15 years ago, something like that wasn’t around. I remember when I wanted to get into open source there were like waiting lists where you cannot connect really a face to a name and to a project and all that. And I’m also super excited and happy to here that you have those Twitch streams and people can join.

David Burns (31:26):
I get paid by BrowserStack, but I’m not promoting them. And so I’m happy to look at anything that fits into my area of expertise. I’m not going to help you with some CSS stuff because CSS is magic to me and anyone that can do it is amazing, but I can’t. But I will help you with how to build a browser and how to do testing, that side and developer experience and scaling systems. So anything and everything is welcome in that stream as long as it’s about helping people.

Darko Fabijan (31:58):
Amazing. Amazing. Great. Thank you so much for your time and good luck.

David Burns (32:04):
Awesome. Thank you.

The post The Past, Present and Future of Browser Testing with David Burns, Core Selenium Contributor appeared first on Semaphore.

View Details

Test-Driven Development (TDD) is an established practice that has been favored by many developers for years. This article helps you understand the history behind TDD, its purpose, its relationship to testing in general, and the benefits you can gain from its use.

Table of contents:

  • How do TDD came into being?
  • Is TDD about testing or software design?
  • TDD as a well-established engineering practice
  • A small example of TDD
  • Which are side effects of TDD?
  • Are there different styles of TDD?
  • Conclusions

How TDD came into being TDD is one of the technical practices of eXtreme Programming. The invention of TDD is usually attributed to Kent Beck; one of the first “extreme programmers”.

This practice shook the common sentiment of programmers at the time. The practice of testing was already widespread, but no one before had ever suggested writing tests before writing the actual code that needs testing. This act is itself counterintuitive if one thinks of TDD as a testing practice. As the author has repeatedly pointed out (and with him many other distinguished programmers), TDD was not born as a testing practice but as a software design practice.

The mantra of the early extreme programmers was to take things that worked well and “exert them to the extreme”. Pair Programming, i.e. the practice of two people writing code collaboratively on the same computer, also arose from this programming movement.

In the same way, TDD was born: “If I test the code I write, I get better quality code: what would happen if I took the process to the extreme: writing tests before the code itself?

If I test the code I write, I get better quality code. What would happen if I took the process to the extreme: writing tests before the code itself?

Kent Beck answered this by developing TDD, and starting a small revolution within the eXtreme Programming revolution.

TDD as a design practice TDD thus began as a practice related to testing, but it soon turned out that the resulting tests were just a nice side-effect. The point of writing tests before code had much more to do with the design of the code itself than its testing.

Writing the test before the code helps the programmer put himself in the shoes of the user, making it easier to create clear software APIs. Using TDD helps make you more comfortable with circumscribing the scope of your code, writing shorter but more focused code, and producing easily-composable modules.

The act of thinking about how to test code early in the process helps with imagining concrete examples. In addition, this allows the developer to focus on concrete cases, avoiding premature generalizations and optimizations.

“The simplest thing that could possibly work” is a phrase you often hear from long-time XP programmers.

Another benefit you get from TDD is rapid feedback on what you produce. Extensive testing is no longer necessary to determine if the code works correctly because there are already tests in place to ensure just that.

eXtreme Programming puts a lot of emphasis on feedback loops. And, among these, shorter cycles that allow you to get quick confirmation are preferable. Of all the XP practices, TDD has the second-fastest feedback loop (second only to Pair Programming), as it provides feedback in a matter of minutes.

Another exciting feature of TDD is the more or less veiled constraint that leads programmers to take smaller and smaller steps. Those who have been doing TDD for a long time surely know the 3 laws of TDD, by Robert C. Martin,, also known as “Uncle Bob”.

In one of his famous articles, Uncle Bob reinforces the TDD process by formulating 3 simple laws:

  • You must write a failing test before you write any production code.
  • You must not write more of a test than is sufficient to fail, or fail to compile.
  • You must not write more production code than is sufficient to make the currently failing test pass.

If you follow this approach, it is clear that you favor incremental development, i.e. writing one test at a time. In this continuous cycle of very short iterations, there is space built in for refactoring. This term is often used as a synonym for “reengineering”, but it has a different meaning, at least in the TDD approach.

In this context, in fact, it represents the most important phase of the whole cycle, where the emphasis is on code quality. In eXtreme Programming, there’s the concept of Simple Design, i.e. the continuous effort to make the code produced simple to evolve.

It is in the refactoring phase that the programmer concentrates on modifying limited portions of code to remove duplication or increase efficiency without changing behavior, which is strengthened by tests that guarantee adequate security. This can be done both in purely practical terms (e.g. introducing a more efficient version of an algorithm), in terms of design, or by modifying or introducing a new abstraction.

TDD was created as a tool for thinking, to increase the focus on small, well-defined portions of code. It helps to proceed in very small steps, adding functionality and value to the software in very small increments in a safe and consistent way. Finally, it enables constant refactoring, one of the most effective practices for keeping software under development in good shape.

TDD as a well-established engineering practice Today TDD is no longer a novelty. There are many teams and developers who rely on this practice to ensure a sustainable pace in product development. Over time, there have also been several studies that support its validity.

An interesting aspect that frequently emerges is related to the adoption of this practice. The initial learning curve may be more or less steep, but it cannot be ignored. Just as the investment of time in test writing, although several studies shows positive ROI.

The evidence is that once developers have obtained sufficient and necessary preparation to enable the practice, projects end up with good results most of the time. This indicates that they are not successful products that enable TDD, but rather that TDD contributes to the success of projects, products and teams.

Another common objection to TDD is about writing a test first. The usual claim is that there are scenarios where writing tests first doesn’t make sense, others where it is very difficult, if not impossible. On the first statement, there is no debate: TDD is not a one-size-fits-all tool, but it has a specific purpose: to support the developer in the design of the software solution.

On the fact that it is difficult -sometimes very difficult- this is also true: oftentimes the defendants here are the underlying code, too rigid to allow the introduction of tests, or the lack of tools. However, both are solvable problems: with a little work, it is possible to facilitate the adoption of the practice with the consequent benefits.

A small example of TDD How do you get started in TDD? The answer alone would deserve an in-depth study. It is, however, possible to get a brief taste.

Let’s assume that we need to write the software for a cash register. The first functionality requested is related to the item scanner: when a specific item is passed (scanned), e.g. an apple, the system must charge a certain amount, e.g. 50 cents.

The following could be a first User Story:

As a cashier, I want a basic checkout system so I can let my customers pay for apples

And these are the related acceptance criteria:

* When I scan an apple, the system charges 50 cents * When I scan 3 apples, the system charges 150 cents

Using the TDD approach, the first thing we need to do is write a test that shows that, currently, we do not have a checkout system that can checkout an apple:

``` import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals;

public class CashRegisterTest {

@Test public void When_I_add_an_apple_the_system_charges_50_cents() { //ARRANGE CashRegister register = new CashRegister(); long expectedCost = 50;

//ACT
register.add("apple");
long actualCost = register.getAmount();

//ASSERT
assertEquals(expectedCost, actualCost);

} } ```

The example is in Java. It allows us to point out a few things.

Testing libraries In the first few lines, we notice references to JUnit libraries. JUnit is a testing framework for Java projects, one of the many xUnit frameworks available.

For those approaching TDD, one of the first things to explore is the testing frameworks available. These days you have so many choices, and you can be certain that there’s at least a “xUnit-like” framework for the language in use.

Test Classes and Methods It is vital to include tests in dedicated files. In Java, the usual practice is to create test classes in dedicated “test” packages:

Classes and test methods make up the “test suite”, which is the set of tests accompanying the software. Therefore, it is crucial to pay proper attention to the organization of the test suite. A good test suite separates tests by scope, making it easy to execute different types of tests separately, e.g. unit tests and End-To-End (E2E) tests.

This separation allows you to run the different types of tests on-demand and independently. It also makes it easy to run them via triggers or schedules in Continuous Integration systems.

The “red test” stage Finally, we have our test:

``` @Test public void When_I_add_an_apple_the_system_charges_50_cents() { //ARRANGE CashRegister register = new CashRegister(); long expectedCost = 50;

//ACT
register.add("apple");
long actualCost = register.getAmount();


//ASSERT
assertEquals(expectedCost, actualCost);

} ```

Note the @Test annotation: a peculiarity that allows the JUnit framework test runner to understand which portions of code should be executed and verified.

The method name is long and unusual, but it is pretty common to use the test method signature to express intent when writing tests.

Finally, we have the “3As“, which help define a good test. The first A, Arrange, reminds us that it’s a good idea to first do the test setup, instantiating objects and any variables we’ll need to use for executing the test.

The second A, Act, focuses on the lines of code that put our System Under Test (SUT) into operation; in this case this is the CashRegister object.

Finally, the third A, Assert, represents the main point: this is where we define the expected result of the test. If the assertion turns out to be true, our test will be satisfied.

In this example, we have our first test arranged to develop the first required functionality, but there is no mention of the production code. This is not an omission: the first test (and the others to come) are written assuming the presence of production code, even if it has not yet been implemented.

This approach, quite counterintuitive at first, is one of the signature characteristics of TDD. It is, therefore, normal to write code that doesn’t even compile and to exploit this situation in order to obtain the so-called “red test”, i.e. the test that, by failing, proves the need to write code.

Only at this point, after having respected the first law of TDD, can we move on to the second, which allows us to write the minimum amount of code necessary to pass the first test.

The “green test” stage How can we pass this first test? By implementing a CashRegister object, obviously. Below is a simple implementation:

``` public class CashRegister {

public void add(String good) {
    //actually I don't need this at the moment :)
}

public long getAmount() {
    return 50;
}

} ```

If you find yourself scoffing at this implementation, you’re probably not the only one. This code allows the system to pass the test, but it’s clear that this CashRegister implementation needs to be revised: as it is, it only works if a customer buys a single apple.

But that’s the point of TDD: to do the minimum necessary. And in this situation, the minimum required to pass the first acceptance criteria is to build a CashRegister that charges 50 cents for one apple.

The refactoring stage Once the assertion of the test is satisfied, it’s time for refactoring. In this first example, it is difficult to highlight this step, since the code is already stripped down to the bare bones.

For the sake of brevity, let’s jump ahead and skip the handful of TDD cycles that would allow us to arrive at the following situation:

  • The system can handle four products: apple, pear, pineapple, and banana
  • For some products, there is a special offer: 3 apples cost 130 cents instead of 150, 2 pears cost 45 cents instead of 60

Let’s see the code implementation:

You can see the additional tests, which allow us to extend our code one bit at a time. They are all green, but the code is starting to creak: the number of conditional statements is growing, and the code is becoming “rigid”.

At this point, it is possible to refactor the code, introducing, for example, the concept of PriceRule, which determines the price for each product, taking into account any current special offers.

Take a look at the result of the refactor below:

With the green tests acting as a safety net, it is possible to refactor the code by introducing a new abstraction (PriceRule). Furthermore, the use of Java Streams has made the product filtering operations more expressive. Now the addition of new products to the catalog is greatly simplified: in essence, it is sufficient to implement a new PriceRule.

This article is not sufficient to give an in-depth explanation of all aspects of TDD, but this small example offers a glimpse of the value that it provides.

The pleasant side effects of TDD We’ve already said it, and now it should be clearer: TDD is a code design technique, not a testing technique. The resulting tests are, in fact, “only a pleasant side effect.”

TDD is a code technique, not a testing technique.

The purpose of TDD is to give the programmer quick feedback about the code they have just written: if something is wrong, a test will signal it. Another goal of TDD is to enable refactoring, as we have just seen: you don’t postpone to the near future (maybe to the distant future…) when refactoring code, paying some of the accumulated technical debt; you act in the moment, when the mind is still fresh and refactoring is both cheaper and easier.

The tests obtained using TDD practices generally fall into the family of unit tests. These kinds of tests are fast: in the order of milliseconds. The result of this approach is hundreds, if not thousands of tests. They continuously run on the programmer’s workstation and in the Continuous Integration (and Deployment/Delivery) pipeline.

The different styles of TDD Over time, different approaches to development have emerged, and with them, different ways of doing TDD.

There are two main TDD approaches. The first is the classical school approach (i.e. the definition comes from XP, and has three names: “Classicist,” “Chicago style,” or “Inside-out“). The second one was born in London a few years later: “Mockist,” “London style,” or “Outside-in.”

These two approaches are not in conflict, on the contrary, they are complementary. The two different approaches can coexist, as they each shine in different situations.

We have already had a taste of the classicist approach: starting from a first small test, we expanded our solution from the inside out, adding functionality from time to time.

The “outside-in” approach works on a different level and uses the TDD “red failing test” approach on more extended functionalities.

For example, look at this test written with the Gherkin syntax, in full Behavior Driven Development (BDD) style:

``` Feature: Supermarket Bundles
To increase revenues as a Supermarket Manager, I want to apply discounts and special offers to my customers

@Bundles Scenario: if a customer buys 3 apples, they pay $1.00 Given a apple costs 50 cents Given the "buy 3 apples, pay 2" promotion When the cashier scans 3 apples Then the cash register charges $1.00 ```

Returning to our previous exercise, we know that there is no “buy 3, pay 2” offers implementation. The outside-in approach makes developers think about the complete functionality, implementing it as simply as they can, even faking some parts when necessary. Inside a bigger “red failing test” like this, development continues via classic short TDD loops. Development continues until this test turns green, which confirms the correct implementation of the required functionality.

Given the larger surface of this kind of test, programmers often use “test doubles” during writing and implementation; an example of which are mocks (hence the adjective “mockist“).

Conclusions This article underlines the main concepts behind TDD:

  • Focus on code design
  • Small steps
  • Short feedback loops
  • Continuous refactoring

It also distinguishes TDD from the other common testing techniques.

Crafting software is somewhere between a science and an art. Mastery of relevant techniques is essential to fully express your art, and TDD is a solid tool to have in your toolbox.

The post Test-Driven Development (TDD): A Time-Tested Recipe for Quality Software appeared first on Semaphore.

View Details

Touching a working Dockerfile can feel like playing with fire. We know that an innocent-looking change can have branching, hard-to-debug consequences. It’s easy to get burned. But change is inevitable, and while commits on Dockerfiles are easy to control, the impact of those changes on the resulting image are not. Fortunately, where there’s a need, there’s a tool. So, let’s elaborate a bit more in our container-diff tutorial.

Introducing container-diff Available in macOS, Linux, and Windows, container-diff (like the name suggests) is diff for container images.

The project, developed by many of the same faces behind Container Structure Tests, does a lot more than just diffing: it can analyze container images, show installed packages, and reverse-engineer the commands used to generate them.

Testing containers Container-diff has the following test modes:

  • Size: shows the total filesystem size.
  • Packages: shows a list of OS-installed packages (only for Debian-based distros), as well as those installed with pip and npm.
  • Filesystem: shows all the files in the image and their size.
  • Layer history: prints the commands that generated each of the layers in the image.

The command to analyze an image looks like this:

container-diff analyze [--type=TEST_TYPE] <IMAGE_NAME>

The tool pulls the image from the registry and unpacks the filesystem into $HOME/.container-diff/cache. Then, the contents are scanned, and a report is printed out.

So, for instance, we can analyze a PostgreSQL image with:

$ container-diff analyze postgres:14 ​ -----Size----- ​ Analysis for postgres:14: IMAGE DIGEST SIZE postgres sha256:3ee027aeb3c8bc4a5870b21 ... 6e27685ac1eab6d4ada 352.9M

The default test is size. Change it to --type=apt to find out which OS-level packages are installed.

$ container-diff analyze --type=apt postgres:14 ​ -----Apt----- ​ Packages found in postgres:14: NAME VERSION SIZE -adduser 3.118 849K -apt 2.2.4 4.2M -base-files 11.1 deb11u1 340K -base-passwd 3.5.51 243K -bash 5.1-2 b3 6.3M -bsdutils 1:2.36.1-8 394K -coreutils 8.32-4 b1 17.1M ​ ... ​ -util-linux 2.36.1-8 4.5M -xz-utils 5.2.5-2 612K -zlib1g 1:1.2.11.dfsg-2 166K

Similarly, you can get a list of globally-installed packages for Node and Python with --type=node and --type=pip.

$ container-diff analyze --type=pip python:3.10-bullseye ​ -----Pip----- ​ Packages found in python:3.10-bullseye: NAME VERSION SIZE INSTALLATION -pip 21.2.4 5.1M /usr/local/lib/python3.10/site-packages -setuptools 57.5.0 2.4M /usr/local/lib/python3.10/site-packages -wheel 0.37.0 94.4K /usr/local/lib/python3.10/site-packages

You can see every file in the image with --type=file, along with its size.

$ container-diff analyze --type=file postgres:14 ​ -----File----- ​ Analysis for postgres:14: FILE SIZE /bin 5.1M /bin/bash 1.2M /bin/cat 42.9K ​ ... ​ /var/spool 7B /var/spool/mail 7B /var/tmp 0

💡 Use --order to show files ordered by size instead of alphabetically.

Finally, the history test shows the Docker layers, which roughly reflect the Dockerfile. The output of --type=history is hard to read, so we’ll format it with sed.

$ container-diff analyze --type=history postgres:14 | sed 's/ */ /g;s/;/\n\t/g' ​ -----History----- ​ Analysis for postgres:14: -/bin/sh -c #(nop) ADD file:16dc2c6d1932194edec28d730b004fd6deca3d0f0e1a07bc5b8b6e8a1662f7af in / -/bin/sh -c #(nop) CMD ["bash"] -/bin/sh -c set -ex if ! command -v gpg > /dev/null then apt-get update apt-get install -y --no-install-recommends gnupg dirmngr rm -rf /var/lib/apt/lists/* fi -/bin/sh -c set -eux groupadd -r postgres --gid=999 useradd -r -g postgres --uid=999 --home-dir=/var/lib/postgresql --shell=/bin/bash postgres mkdir -p /var/lib/postgresql chown -R postgres:postgres /var/lib/postgresql ​ ...

Comparing containers We’re only scratching the surface so far. Container-diff really shines when comparing images. The command for this is:

container-diff diff [--type=TEST_TYPE] <IMAGE1> <IMAGE2>

Let’s see some use cases for image comparison.

Use case 1: generating a changelog Container-diff works great for generating changelogs. And, as we’ll see in the next section, the output format can be customized using a template.

We can list what changed at the OS level:

$ container-diff diff --type=size --type=apt postgres:13 postgres:14 ​ -----Apt----- ​ Packages found only in postgres:13: NAME VERSION SIZE -postgresql-13 13.5-1.pgdg110 1 46.9M -postgresql-client-13 13.5-1.pgdg110 1 6.3M ​ Packages found only in postgres:14: NAME VERSION SIZE -postgresql-14 14.1-1.pgdg110 1 48.9M -postgresql-client-14 14.1-1.pgdg110 1 7.1M ​ Version differences: None ​ -----Size----- ​ Image size difference between postgres:13 and postgres:14: SIZE1 SIZE2 350.2M 352.9M

In the same vein, we can compare globally-installed Node packages:

``` $ container-diff diff --type=node node:16 node:17

-----Node-----

Packages found only in node:16: None

Packages found only in node:17: None

Version differences:
PACKAGE IMAGE1 (node:16) IMAGE2 (node:17)
-npm 8.1.0, 8M 8.1.2, 8M ```

Or changes in Python packages:

``` $ container-diff diff --type=pip python:3.6.15-buster python:3.10-bullseye

-----Pip-----

Packages found only in python:3.6.15-buster:
NAME VERSION SIZE
-argparse 1.2.1 87.1K
-mercurial 4.8.2 9.5M
-wsgiref 0.1.2 98.7K

Packages found only in python:3.10-bullseye: None ```

Use case 2: troubleshooting containers Debugging a failing container is easy when we have a healthy image to use as a reference. To see all the file changes, run container-diff with --type=file:

``` $ container-diff diff --type=file myapp/myservice:v1 myapp/myservice:v2

-----File-----

These entries have been added to myapp/myservice:v1:

FILE SIZE
/app/node_modules/fsevents 186.2K
/app/node_modules/fsevents/LICENSE 1.1K
/app/node_modules/fsevents/README.md 2.9K

These entries have been deleted from myapp/myservice:v1:

FILE SIZE
/app/.npm/_cacache/index-v5/ce/9f/58654f1 310B
/app/.npm/_cacache/index-v5/3d/b7/10f6556 309B
/app/.npm/_cacache/index-v5/7e/eb/c1538ff 308B

These entries have been changed between myapp/myservice:v1: and myapp/myservice:v2:
FILE SIZE1 SIZE2
/app/package-lock.json 554.6K 554.6K
/app/node_modules/.package-lock.json 297.7K 298.1K
/app/node_modules/clean-css/History.md 77.5K 77.8K ```

Once the problematic file is identified, you can compare the files in both containers to see what changed.

$ container-diff diff <IMAGE1> <IMAGE2> --type=file --filename=PATH/TO/FILE

Use case 3: test-driving new containers You can run container-diff to preview the impact of your changes in a build. For instance, to quickly try out different base images or play with the Dockerfile. You can iterate until you’re sure you’ve got it right.

Container-diff is not limited to images in remote repositories. You can analyze any local image by prefixing its name with daemon://.

container-diff diff --type=TEST_TYPE daemon://IMAGE_NAME:TAG daemon://IMAGE_NAME:TAG

Imagine that you’re building a container for a Ruby app and want to try upgrading from Ruby 2.7 to 3.0. As a Ruby developer, you know what to expect from the language side, but can you say the same about the container?

To answer the question, let’s compare the respective Ruby images:

``` $ container-diff diff --type=size --type=apt ruby:2.7.4-bullseye ruby:3.0.2-bullseye

-----Apt-----

Packages found only in ruby:2.7.4-bullseye: None

Packages found only in ruby:3.0.2-bullseye: None

Version differences: None

-----History-----

Docker history lines found only in ruby:2.7.4-bullseye:
-/bin/sh -c #(nop) ENV RUBY_MAJOR=2.7
-/bin/sh -c #(nop) ENV RUBY_VERSION=2.7.4
-/bin/sh -c #(nop) ENV RUBY_DOWNLOAD_SHA256=2a80824e0ad6100826b69b9890bf55cfc4cf2b61a1e1330fccbcb30c46cef8d7

Docker history lines found only in ruby:3.0.2-bullseye:
-/bin/sh -c #(nop) ENV RUBY_MAJOR=3.0
-/bin/sh -c #(nop) ENV RUBY_VERSION=3.0.2
-/bin/sh -c #(nop) ENV RUBY_DOWNLOAD_SHA256=570e7773100f625599575f363831166d91d49a1ab97d3ab6495af44774155c40

-----Size-----

Image size difference between ruby:2.7.4-bullseye and ruby:3.0.2-bullseye:
SIZE1 SIZE2
819.2M 835.8M ```

Compare that with changing the OS flavor in the Node image. What happens if you want to swap out Bullseye for Bullseye Slim?

``` $ container-diff diff --type=size --type=apt --type=node node:17-bullseye node:17-bullseye-slim

-----Apt-----

Packages found only in node:17-bullseye:
NAME VERSION SIZE
-autoconf 2.69-14 1.8M
-automake 1:1.16.3-2 1.8M
-autotools-dev 20180224.1 nmu1 157K
...

-----Node-----

Packages found only in node:17-bullseye: None

Packages found only in node:17-bullseye-slim: None

Version differences: None

-----Size-----

Image size difference between node:17-bullseye and node:17-bullseye-slim:
SIZE1 SIZE2
942.9M 230.7M ```

Comparing regular Bullseye vs. Slim shows that:

  • Node stays the same.
  • Slim image is about 12 MB smaller.
  • The smaller image has a long list of missing packages.

This information will help you decide which is the best version for you. It makes sense to pick Slim in order to reduce the attack surface if you don’t need the extra packages.

Extending and customizing container-diff When the default text output is not enough, we can write an output template. You can see the examples in the built-in template file.

The --format option lets us customize how information is printed out, giving us a way to export the data to other formats, such as CSV:

``` $ container-diff diff python:3.9-bullseye python:3.10-bullseye --type=pip --format='
package,{{.Image1}},{{.Image2}}
{{range .Diff.InfoDiff}}{{.Package}},{{range .Info1}}{{.Version}}{{end}},{{range .Info2}}{{.Version}}{{"\n"}}{{end}}{{end}}
'

package,python:3.9-bullseye,python:3.10-bullseye
pip,21.2.4,21.2.4
setuptools,57.5.0,57.5.0
wheel,0.37.0,0.37.0 ```

When custom formats are not enough, container-diff can be extended by writing your own differ. You’ll need solid knowledge of Go for that, though.

Automated container testing with CI/CD How does container-diff help us deploy safely? Well, if you’re doing continuous integration, you’re probably deploying several times a day, which means each new container is only a little bit different from the previous one.

Following that logic, we can assume that if too many things change at once, it may be a signal that further analysis is needed before deployment. Maybe some unexpected file snuck into the build and the image size doubled, or the base image was updated in the registry and unexpectedly shipped with different libraries.

We have to strike the right balance between stability and mutability. Every team will have different thresholds but, as a starting point, let’s say that we’ll reject images that:

  • Grow more than 10% in size.
  • Have different OS libraries.
  • Have different globally-installed Node packages.
  • Were built from a different Dockerfile.

Gauging change rate between images We can evaluate the changes by running container-diff with --json and processing the output. The format is:

{ "Image1": "foo", "Image2": "bar", "DiffType": "Test_Type", "Diff": { // Differences Object } }

We can process the report with a combination of shell scripts and jq, the JSON Query CLI tool. First, run all the tests at once and save the output in a file:

$ container-diff --type=size --type=apt --type=node --type=history --json > diff.json

Then, pipe the output to jq. You can filter the results per test by selecting DiffType. Use the following command to see the APT changes:

$ jq '.[] | select(.DiffType=="Apt")' diff.json

You can get the total number of changed packages by appending .Diff.Packages1 + .Diff.Packages2 | length to the query.

$ jq '.[] | select(.DiffType=="Apt") | .Diff.Packages1 + .Diff.Packages2 | length' diff.json

💡 You can try jq online at jq play.

Once we have all the jq queries ready, we can write a script that runs the differ, filters the results, and fails if the changes exceed certain thresholds.

```

!/bin/bash

Compare container and stop pipeline when changes exceed control parameters

Parameters expected:

$ALLOWED_APT_CHANGES - max number of allowed APT packages changed

$ALLOWED_HISTORY_CHANGES - max number of Dockerfile commands changed

$ALLOWED_NPM_CHANGES - max number of NPM packages changed

$MAX_GROWTH_RATIO - percentual growth size allowed (0 is no growth, 100 is double size)

set -ex

image1=$1
image2=$2

diffile=$(mktemp XXXXXX.json)

container-diff diff \
--type=history --type=node --type=size --type=apt --json \
"$image1" \
"$image2" \
> ${diffile}

changes_apt=$(jq '.[] | select(.DiffType=="Apt") | .Diff.Packages1 + .Diff.Packages2 | length' ${diffile})

changes_history=$(jq '.[] | select(.DiffType=="History") | .Diff.Adds + .Diff.Dels | length' ${diffile})

changes_npm=$(jq '.[] | select(.DiffType=="Node") | .Diff.Packages1 + .Diff.Packages2 | length' ${diffile})

When sizes are equal jq returns a string "null"

size1=$(jq '.[] | select(.DiffType=="Size") | .Diff[0].Size1 ' ${diffile})
if [ "$size1" = "null" ]
then
size_ratio=0
else
size_ratio=$(jq '.[] | select(.DiffType=="Size") | 100 * .Diff[0].Size2 / .Diff[0].Size1 - 100 | floor' ${diffile})
fi

Evaluate thresholds

if [ $changes_apt -gt $ALLOWED_APT_CHANGES ] \
|| [ $changes_history -gt $ALLOWED_HISTORY_CHANGES ] \
|| [ $changes_npm -gt $ALLOWED_NPM_CHANGES ] \
|| [ $size_ratio -gt $MAX_GROWTH_RATIO ]
then
exit 1
else
echo OK
fi ```

Adding a change-control job to CI/CD Where were we? Let’s see, we have two images and a script to compare them. What we need now is a CI/CD pipeline that builds the image. Semaphore has the capabilities that we want for this task. If you’ve never used Semaphore before, I recommend checking out the getting started guide.

Open the workflow editor and add a block after the container image build step. Then, add the following commands in the job:

curl -LO https://storage.googleapis.com/container-diff/latest/container-diff-linux-amd64 sudo install container-diff-linux-amd64 /usr/local/bin/container-diff echo "${DOCKER_PASSWORD}" | docker login -u "${DOCKER_USERNAME}" --password-stdin checkout chmod a+x container-diff-test.sh && ./container-diff-test.sh "${DOCKER_USERNAME}"/mycontainer:latest "${DOCKER_USERNAME}"/mycontainer:$SEMAPHORE_WORKFLOW_ID

This job installs container-diff in the CI machine, logs in the Docker Hub registry (you’ll need to activate a secret), clones the repository, and runs the comparison script. Change the parameters in container-diff-test.sh as needed. In this case, we’re comparing the latest image against the one tagged with the unique id $SEMAPHORE_WORKFLOW_ID.

Container control job That’s it! You can complete the pipeline with the deployment method of your choice.

Example pipeline If you need inspiration for setting up a deployment, check these resources to learn how you can deploy with Semaphore:

  • How To Deploy a Go Web Application with Docker
  • Continuous Integration and Delivery to AWS Kubernetes
  • How to Release Faster with Continuous Delivery for Google Kubernetes
  • A Step-by-Step Guide to Continuous Deployment on Kubernetes

Wrapping up Container-diff is yet another quality tool to keep containers in check. Remember, when using containers, you’re responsible for the whole mini OS that comes with them, not just the code.

Increase your Docker-fu with these posts:

  • Structure Testing for Docker Containers
  • CI/CD with Docker and Kubernetes.
  • Beyond Docker with Earthly
  • Kubernetes vs. Docker: Understanding Containers in 2021

Thank you for reading!

The post Change Management for Containers appeared first on Semaphore.

View Details

In this podcast episode, I welcome Amjad Masad, co-founder and CEO of Replit, the collaborative browser-based IDE. We talk about how Replit started, the technologies behind the product, but also how accessible tools change lives and help to make programming a passion.

Key highlights: * Making accessible tools as a life goal * Replit doing magic * The technical side of Replit * What happens when you start a REPL? * Interactive programming as a way to converse with a computer * Prybar by Replit * What does a user journey with Replit look like? * What is Nix?

Listen to our insightful conversation or read the edited transcript.

You can also get Semaphore Uncut on Apple Podcasts, Spotify, Google Podcasts, Stitcher, and more.

Like this episode? Be sure to leave a ⭐️⭐️⭐️⭐️⭐️ review on the podcast player of your choice and share it with your friends.

Edited Transcript Darko Fabijan (00:02):

Hello and welcome to Semaphore Uncut, a podcast for developers about building great products. Today, I’m excited to welcome Amjad Masad. Amjad, thank you so much for joining us. Please go ahead and introduce yourself.

Amjad Masad (00:15):

My name is Amjad, I am the co-founder and CEO of Replit. Replit is a collaborative end browser IDE. It’s also a community of developers, a social platform where you can publish your software and collaborate with other people and it’s a great place to get introduced to programming.

Before that, I worked at Facebook. I built developer tools at Facebook. I worked on Babel JS, React Native and the React Native packager, which became an open source project called Metro. I started that. At Facebook, I started a team called JavaScript Infrastructure Team, and we built a lot of the tools that people use today. Babel Adjust, a lot of the JavaScript tools.

And before that, I was at Codecademy. At Codecademy, I did a similar thing to Replit, with a lot more focus on guided education. But the idea is that you should be able to get a REPL in the browser and learn how to code, just remove all the barriers to entry and let people experience the joy of programming as early as possible.

But the idea is that you should be able to get a REPL in the browser and learn how to code, just remove all the barriers to entry and let people experience the joy of programming as early as possible.

-Amjad Masad, CEO of Replit

Making accessible tools as a life goal Amjad Masad (01:38):

I guess my life’s goal ended up being making accessible tools that are enjoyable for people to use. Even with my work at Facebook, I focused on really making software development fun, easy, accessible.

The vision I had is that the Internet is this great equalizer, it’s the place where, for the first time in the history of humanity, we have something that allows everyone anywhere to be able to contribute to something larger than yourself.

-Amjad Masad, CEO of Replit

I come from Jordan and the vision I had back then is that the Internet is this great equalizer, it’s the place where, for the first time in the history of humanity, we have something that allows everyone anywhere, regardless of who you are, your socioeconomic status, your gender, your color, to be able to contribute to something larger than yourself and to be able to reap the benefits of this new digital economy.

The vision I had is that for us to create a level playing field to allow everyone in the world to participate, the tools need to be easier and more accessible, so I dedicated my career to building these tools.

Darko Fabijan (02:42):

Great and very inspiring. Yeah, I can relate to this. Many times whenever I would switch techs and try to get into something new, that barrier of entry of setting up things was actually pretty high.

For instance, I started in early nineties with Visual Basic 4, and then later, I wanted to get into Delphi, didn’t and then C++, and then, over to college and career. I mean, in hindsight, it’s simple and you should solve it, you should get over it, but in reality, in those starting phases, that can really slow you down or discourage you from moving forward at all.

Replit doing magic Amjad Masad (03:25):

Yeah. It doesn’t make sense to acquire that type of investment. Let’s say you’re learning how to drive a car, we don’t ask you to build the car or assemble it, right? But in programming, we do ask you to assemble the tools before you start. One of the critiques that Replit gets is that okay, we’re doing magic, we’re hiding complexity, we’re discouraging our users from discovering things, but that’s not true at all.

Let’s say you’re learning how to drive a car, we don’t ask you to build the car or assemble it, right? But in programming, we do ask you to assemble the tools before you start.

-Amjad Masad, CEO of Replit

Amjad Masad (03:50):

What we see is that our users, maybe at the first steps, they kind of don’t do that, but once they’re proficient in programming, they general really want to go understand how package managers work, how IDEs work, how interpreters work, they even write interpreters, they even write databases.

Back to the car analogy, I’m really into cars and when I first started driving cars, I did not know anything about engines or anything like that. And then, when I started getting into cars and I started tuning the car and buying parts and really figuring out how engines work and how gears work and all of that, really, it’s about progression. It’s about getting people in the door, seeing the value proposition, seeing the fun of it and then you reveal more of the complexity as they go on.

Darko Fabijan (04:48):

Yeah. And some people will decide to go super deep, but for some people, that will not be of interest, they might be more interested in building something else and that’s fine.

Amjad Masad (04:59):

That’s true. I mean, it should be optional. Yeah.

Darko Fabijan (05:43):

I have a six year old daughter and this part about education reminded me of that, of how I introduce new people to things and it has to be interesting. And on the surface, everything is just fine. Let me decide whether I am going to dive super deep and don’t store me in the beginning with, “Okay, you need electricity. Let me tell you how electricity is made. And then, next time, I’m going to show you how we built the program.”

Amjad Masad (06:16):

Exactly. Yeah.

The technical side of Replit Darko Fabijan (06:18):

Great. And you mentioned that you are also super interested in all the technology which is behind this, networks of things and people. Can you talk more about the technical side of Replit?

Amjad Masad (06:40):

Replit is at the intersection of everything I am interested in and it’s a fascinating set of problems. At the intersection of compilers and developer tools and systems programming and interface design and UI and people, networks, social networking. Everything I was interested in, this became a center of it all and it’s been super fun to work on all these ideas and the breadth that we cover is absolutely fantastic, but that’s what I was excited about from the start.

What happens when you start a REPL? Amjad Masad (07:13):

I guess, to talk about the technical stuff first, the one thing to start with is what happens when you go to Replit and you start a REPL. A REPL is what we call the environment. The name, obviously, comes from read-eval-print loop.

It’s a concept from the fifties that was started with LISP. LISP was the first dynamic programming language invented and one of the really interesting things about it is that the code is data and you can evaluate the code in the running program, right? So like eval() in JavaScript or Ruby or something like that.

Amjad Masad (07:52):

That was the first instance of that and they had these primitives like reading something from the terminal, printing something out and evaluating it, so you put them together and you have a one liner that’s really super powerful, which is read-eval-print loop. You read something in the terminal, you evaluate it, you print it and you repeat. And you can bootstrap, essentially, an interactive programming environment using one line of coded LISP, which is just a fascinating concept in itself.

Amjad Masad (08:20):

I was really fascinated by it and I was really interested in how we can apply that concept to daily programming and I was really frustrated by how a lot of programming is about sitting down, thinking out a lot of ideas and plans for how to do something, designing an algorithm, and then going in and writing hundreds, maybe thousands of lines of code, and then running the program and then figuring out that one of your assumptions is wrong and then you have to repeat the entire process.

Interactive programming as a way to converse with a computer Amjad Masad (08:56):

So interactive programming, for me, was about revealing the program as you go, it’s almost like a conversation with the computer. That was the idea I was starting with, so with Replit, the idea was, let’s do it for all programming languages and let’s make that the base of how you start coding.

Interactive programming to me was about revealing the program as you go, it’s almost like a conversation with the computer.

-Amjad Masad, CEO of Replit

When you start a REPL, what happens is, we connect to a backend service, this backend service grabs a container, typically from a running container pool because we want to keep them hot and ready. If it’s a new project, it’ll start a new file system for you and we’ll start storing that on GCS, which is S3 on GCPU, and it will drop in a program that we call pid1.

So pid1 is, as it’s called, the first process in the container. That process is responsible for communicating all the way from the container to the IDE to the REPL and it’s responsible for all the operations that you might want to do as you’re coding.

Amjad Masad (10:09):

We use a protocol that’s very similar in its nature to SSH, meaning there’s a control channel where it allows you to open other channels. Everything we do in the programming environment is based on a channel. Say, you want to write a file, you open a file writer channel and as you start coding, that’s sending messages across. When we’re dealing with text, we’re typically dealing with operational transform messages. Operational transform is the gold standard for collaborating on text. There are CRDTs, which I think are more popular now, but those are more generic, more complex, and in some ways, a lot more powerful as well, but OT is kind of simple for textual based collaboration.

Amjad Masad (10:56):

So you open that channel. Those channels also have a built-in mechanism for collaboration, because Replit is a collaborative editor at heart, so you can broadcast these messages to other people on the system, they’re all connected to the same container.

And then, the container starts, pid1 starts and you open a file, you open a channel, you start coding, other people might see it if they’re on the same session. And then, we built a lot of layers on top of different languages to make them behave the same. Again, we wanted to create this REPL experience, this interactive experience.

Prybar by Replit Amjad Masad (11:34):

For example, we built this layer called prybar. Prybar makes a lot of dynamic language top levels, the interactive REPL. So prybar has support for Node, Groovy, Python, Lua, a lot of interactive languages. That gets started on the console side as well and as you’re coding, you can run the code, you can see the code live and then you can interact with the code via the REPL. And then, again, on the abstractions, we try to abstract a lot of the layers that make up any kind of tooling you’d want in the workspace.

Amjad Masad (12:15):

For example, package management. We built this thing called universal package manager and this is a front end for all the different package managers and different languages. Again, a lot of what we do at Replit is taking best practices from the different languages and creating layers to make them behave the same or in a predictable way.

So whether you’re in Python, Ruby, Node, Go, Rust, whatever. When you want to install a package, it all follows the same thing. UPM has really interesting things like it will detect that you require a package and it will install it for you, for example, which makes it really fun for beginners to not have to deal with package managers.

Amjad Masad (12:58):

The entire user journey begins with this, getting that container and then having all these tools ready for them and having a really simple interface that they can get started with. But it’s also powerful and it’s also progressively revealing its complexity if you need it.

For example, when you’re coding and you open a server port, you start an SUP server on whatever port. We figured that out in the container and then I think we used to use the Linux audit log, but I think we moved to something else that I’m kind of blanking on. I think we trap the SYSQL somehow and figure out that you’re trying to do a server.

And then, we open a web view for you and then the container gets started proxying requests into the container. So now, we’re building a web app, not only you’re building a web app, but also, this web app is hosted for you.

Amjad Masad (13:54):

We take this approach to every other thing that you might want to do, whether it’s graphics, programming or other things like that, so that’s the core piece of it. In terms of container management, a lot of people ask us like, “Do you use like Kubernetes or something like that?”

And actually, when we started building this, Kubernetes was not the phenomenon that it is right now and actually, I don’t think it actually is very good for our use case. One, because of performance and one is because we need to be starting and taking down all these containers all the time.

So we built our own container manager that functions like a distributed system. When you want to grab a container, you could get a container at a cluster that’s geolocated closely to you. When you invite someone to collaborate with you, they go to the cluster closest to them, the data center closest to them, and then, there’s peering inside the system that allows them to work with you.

Amjad Masad (14:54):

That aspect of it becomes really interesting in how we bring things up and bring them down, how we do locks so that you don’t have two containers running it at any time, which we had a big problem with at some point, so to distribute locking. And then, how we manage cost because we want to provide a free service, but we’re providing free computers on the internet. So you’re probably dealing with that, in Singapore, there’s a lot of crypto mining, there’s a lot of issues like that. Security’s a very deep topic for us, but we’ve done well on it. That’s another interest of mine starting out as fighting fraud and things like that. That’s the snapshot of the system.

What does a user journey with Replit look like Darko Fabijan (16:11):

As you were talking about this, I was trying to imagine a journey of a user starting out, maybe learning things and then sticking with Replit and progressing through their maybe just education as a student or young professional and so on.

Amjad Masad (16:29):

I can tell you, I can paint a picture if you’d like. One of my favorite users started when they were nine years old on the platform when we started the company, so around 2016 and now, they are 14, 15, and they’re still coding in Replit and you can go to their profile and you can see their first line of code being, “Hello, world.” And then, you can see the latest application being a Django application and this is the first platform, I think, in the history of programming that shows you the progression of someone from their first line of code to their first application to potentially their first business.

Amjad Masad (17:10):

When they came, probably in 2016, Replit was a single file and a console. It was purely a REPL. In 2017, I think we added multiple files. In 2018, we added the packager. In 2019, we added web hosting and before that, maybe we added multiplayer.

So year over year, the platform is getting more exciting, more interesting for them to explore and to hack. Also, there are more people that they could learn with, that they could interact with and they form friendships and I think one of the things that I did not anticipate is Replit being a place to form deep, meaningful connections with people. And we see folks, especially on the younger side saying, “This is my community. This is my friendships. This is what I do.” That’s been very satisfying.

Replit is a place where people form meaningful connections with other people.

-Amjad Masad, CEO of Replit

Darko Fabijan (18:18):

I could imagine someone in elementary school or high school, so I’m the only one in my class interested in programming. Where do I find another programmer of my age?

Amjad Masad (18:30):

Exactly, exactly. Yeah, it’s still the problem. It’s still the problem.

Darko Fabijan (18:34):

From the side of starting with technology, it’s super interesting what you said. I don’t know if I would really enjoy seeing it, but I would maybe want to see what my progression was and what was those lines over almost two decades, that I wrote, but in terms of technology changes and switches and going in loops, did you guys maybe did some analysis, observe something interesting that first line with JavaScript and they ended up in making games in C++ or, I don’t know.

Amjad Masad (19:08):

Yeah, no, we should. We haven’t done that and we haven’t really invested too much in data analysis or just giving people insights on their own behavior and their own profiles, but we have all that data and I think we should be able to do it, but just anecdotally, I see a lot of that zigzagging happening.

I mean, you look at the industry in general and you see that zigzagging happening. When I started professionally coding, everyone was interested in doing type languages and compiled languages were table stakes. This is what you had to learn, this is what you had to do. You had to write safe programs. And then there was this rebellion led by PHP and then later on Python and Ruby and then JavaScript where no dynamic, everything, developer productivity is more important than bugs.

Amjad Masad (20:01):

And then, now, it’s not exactly a zigzag, but it is a synthesis of these two ideas where now, we have things like optional typing, like TypeScript, Dart, Flow. Python has one, Stripe did one for Ruby.

We’re like, “Oh, actually dynamic programs are very, very useful early on as a startup or writing a project, but once you hit scale, it starts really holding you back.” Now, typed languages are all the rage again, so you see that in the larger ecosystem and you definitely see a microchasm of it at Replit where what people are interested in and it changes all the time.

What is Nix? Amjad Masad (20:49):

And maybe we can get to it as you please, but this is why we’re investing in Nix. Nix is a purely functional package manager and it’s solving this problem where creating an operating system instance is this very tricky, unsecure, rolling the dice type thing and their idea is that, no, actually, when you’re creating your computer, essentially, everything should be reproducible and reproducibility is great.

Amjad Masad (21:23):

It’s also, everything should be pure so you know exactly what happened, right? So that you don’t get man in the middle or you don’t get a random package as we’ve seen with some of the stuff that happened in NPM around packages getting taken over by malicious actors.

Nix’s kind of solved this problem and when we looked at Nix, what we really saw was a solution to one of our deepest pains, one of our pains and by the way, this is a pain for all paths like platforms, which is you want to create this one time for your users that has all the tools that they might need.

Amjad Masad (22:03):

The way Heroku works around that is that, oh, we’re going to create these stacks, they call them these interesting names, Cedar or whatever. They make you upgrade every once in a while. I need Node 16, Python 36, whatever. They can’t really give you that, they have these run times that you have to upgrade to a lockstep and they make the decision of what versions of things, you can’t do things à la carte.

And we went down that path so we built this thing called Polygott. It’s a play on polyglot. And we were like, “We’re going to install everything under the sun that you might need.

Amjad Masad (22:45):

And turns out it’s a really bad idea because one, it’s impossible to upgrade. When you want to upgrade a package, there’s a 60%, 70% likelihood that you’re going to break something else. And it’s not reproducible. If you break something, it’s really hard to roll back and that’s obviously something that Nix could solve. Also, the images that we were deploying were 10 gigabyte images because it had all these random tools, right?

Amjad Masad (23:17):

So we’re like, “What we really want, we want a description of the set of tools that you want and we want the output to be reproducible, cashable, something you can really trust over time.” So now, when we want to support a language, we simply write the Nix expression, “I want Node 16 with Gits, with all these other tools,” and it generates this image that’s cashable for us.

We actually cashed all the Nix packages. We have the shared drive that we mount on every container. And then, when the Nix expression runs, we cash that as well. So we’re creating these templates now that allow us to support all these variations of run times. It’s the first Pez-like platform to give you complete control over your run time, but also is instant. You don’t have to go prepare a VM image and snapshot that. No, you instantly can create these things.

Amjad Masad (24:22):

I’ll give you an example of how this really benefited us. Right now, Web3 is a trend that’s really big, right? So developing smart contracts on Ethereum and some people at the company are really passionate about Ethereum solidity and things like that and there’s a lot of demand, we want to be able to support that and we were able to get something working in 10 minutes because we were describing the package we already cashed, we were describing that and it generated a solidity the environment in 10 minutes.

Amjad Masad (24:57):

It took us a little bit longer to add syntax highlighting, to make the developer experience really, really cool, but in a matter of two, three weeks, we were able to release this new product, essentially, to the market and the reception was absolutely crazy. Our tweets got something like 2,000 likes, it was featured on every newsletter for crypto and it’s been used thousands of times. It’s getting us a lot of new users that we wouldn’t have had exposure to.

One, we believe that Nix is the future. It’s really obvious to us. We’re kind of surprised that it hasn’t really taken off in a major way just yet. We’re investing in it, we donated $25,000 to the foundation. We try to help in any way we can, help promote it and write about it, but it’s also just made our system a lot more hackable. If you don’t like something, you can change it. You can totally own your environment.

Darko Fabijan (25:52):

I can say that at this point in time, we are kind of opposition just because I would say that our current experience with Nix is that people are bothering us.

Amjad Masad (26:05):

For support?

Darko Fabijan (26:06):

Yeah, yeah. What more can we do to support them better. And I, in just maybe the last couple of weeks or a bit more, had some conversations around that with customers and they’re super happy and super enthusiastic and we are early adopters and we think it’s amazing and great and we didn’t have a chance to dive deeper into that.

But a lot of what you are describing is also a pain point for our customers and then for us to support them because those applications are composed out of many different versions of languages, databases, some other dependencies that they want to have and they want to cash them, they want them fast, but it’s not like that. We can have hundreds of instances waiting just for them with their particular set of dependencies.

Amjad Masad (27:01):

Yeah. My prediction is, a year or two from now, you’re going to arrive at the same technical solution that we have, to have a large hard drive with casting all the Nix packages and a lot of your customers are going to be using Nix. That would be my prediction, but who knows? I think it’s just a matter of time when the world really wakes up to it. And I think a lot of misconceptions about, oh, Dockers are producible and cashable, but it’s really not the same thing.

Darko Fabijan (27:26):

Yeah, it’s not. In our industry, there was a period of time that some foreign competitors ditched VMs completely, everything had to be a Docker image, you cannot do anything outside of Docker image and all that. And a lot of people just want to get that instance that has that Ruby 2.6.what and that PostgreSQL 11 and they want those two things and they don’t want to bother with packaging, maintaining their images and upgrading them and all that. A lot of that in our industry ends up being such a huge hype as Docker is great. It’s great for many use cases, but it’s not that every single person wants to use that all the time. You know?

Amjad Masad (28:11):

It’s not a silver bullet and it’s also not improving at the rates that it used to be improving at, I think, because the company is struggling and things like that. There are alternatives now. There’s something called Pod and things like that, so there’s still some innovation there.

The future of Replit Darko Fabijan (28:26):

Yeah, but I also hear a feeling that it hit a certain level and I don’t see what would be that that would involve someone investing so much more into that as to improve it to a significant new level at this point in time. Okay, great. This was a fantastic journey. Is there something that you’re particularly looking forward to when it comes to Replit?

Amjad Masad (28:49):

A lot of the things that I described are still in active development and I think once we really nail down the next experience, the way I’d like to describe Replit is that it’s going to be a bootstrapped environment. We’re going to build the environment in itself.

Bootstrapping goes all the way back to Douglas Engelbart, known for the mother of all demos. Bootstrapping right now is really known about compilers, right? You write the compiler, the language starts working, you rewrite the compiler in the same language and then you throw away the old compiler and now you have a bootstrapped compiled, which is a fascinating idea. That it just works, it’s just an insane idea, right?

Amjad Masad (29:31):

Bootstrapping, actually, in the original sense of the term, is used a lot more broadly. The interesting thing about Replit right now as a bootstrapped environment is that we’re going to be able to build Replit in Replit and we’re going to be able to use it internally to build Replit in Replit, so it’s a bunch of recursive cycles and product is improving at a pace that wasn’t improving at before and I think we’re going to really accelerate development. We’re going to make it a lot more powerful. If you tried Replit before and it didn’t work for a use case, I would really try it now again or try in a few months because the rate of improvement is going to be astronomical.

Darko Fabijan (30:05):

Great. We’re excited to hear that. Amjad, thank you so much for joining us. Good luck with Replit and let’s talk again at some point.

Amjad Masad (30:13):

My pleasure and thank you for Semaphore for, we’re very, very happy customers.

Darko Fabijan (30:18):

Thank you. Bye bye.

Amjad Masad (30:20):

Bye.

The post Under the Hood of Replit with Amjad Masad appeared first on Semaphore.